svg_conform 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +32 -14
- data/lib/svg_conform/classification_cache.rb +36 -0
- data/lib/svg_conform/profile.rb +3 -0
- data/lib/svg_conform/requirements/base_requirement.rb +83 -0
- data/lib/svg_conform/requirements/id_reference_requirement.rb +29 -28
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +41 -21
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +16 -9
- data/lib/svg_conform/sax_validation_handler.rb +52 -68
- data/lib/svg_conform/validation_context.rb +51 -16
- data/lib/svg_conform/version.rb +1 -1
- data/spec/fixtures/svg_1_2_rfc/expected_errors/ietf_test_violations.yml +47 -0
- data/spec/fixtures/svg_1_2_rfc/inputs/ietf_test_violations.svg +28 -0
- data/spec/spec_helper.rb +0 -5
- data/spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb +150 -0
- data/spec/svg_conform/validation_context_spec.rb +305 -0
- data/spec/svg_conform_spec.rb +156 -0
- metadata +6 -2
|
@@ -8,21 +8,39 @@ require_relative "validation/structural_invalidity_tracker"
|
|
|
8
8
|
require_relative "validation/node_id_manager"
|
|
9
9
|
require_relative "validation_result"
|
|
10
10
|
require_relative "references"
|
|
11
|
+
require_relative "classification_cache"
|
|
11
12
|
|
|
12
13
|
module SvgConform
|
|
13
14
|
# SAX event handler for streaming SVG validation
|
|
14
15
|
# Processes XML events and dispatches to requirements
|
|
16
|
+
#
|
|
17
|
+
# == Requirement classification
|
|
18
|
+
#
|
|
19
|
+
# The handler classifies requirements into two categories:
|
|
20
|
+
#
|
|
21
|
+
# 1. Immediate validation requirements (14) - validate during parsing
|
|
22
|
+
# - These requirements implement +validate_sax_element+ and validate
|
|
23
|
+
# each element as it is encountered
|
|
24
|
+
# - No state is maintained between elements
|
|
25
|
+
#
|
|
26
|
+
# 2. Deferred validation requirements (3) - collect data, validate at end
|
|
27
|
+
# - These requirements implement +collect_sax_data+ and +validate_sax_complete+
|
|
28
|
+
# - State is stored in requirement-specific State classes via +context.state_for+
|
|
29
|
+
# - Validation happens after parsing is complete, when all data is available
|
|
30
|
+
#
|
|
31
|
+
# == State management
|
|
32
|
+
#
|
|
33
|
+
# Each handler instance has its own cache and fresh requirement instances,
|
|
34
|
+
# ensuring no state leakage between validations.
|
|
35
|
+
#
|
|
36
|
+
# Profile reuse for multiple validations:
|
|
37
|
+
# - Profile is created once with requirement instances (configuration from YAML)
|
|
38
|
+
# - Each validation creates a fresh SaxValidationHandler with fresh ValidationContext
|
|
39
|
+
# - ValidationContext creates fresh State instances for deferred requirements
|
|
40
|
+
# - No state pollution occurs across validations
|
|
15
41
|
class SaxValidationHandler < Nokogiri::XML::SAX::Document
|
|
16
42
|
attr_reader :result, :context
|
|
17
43
|
|
|
18
|
-
# Class-level cache for requirement classifications by profile class
|
|
19
|
-
@classification_cache = {}
|
|
20
|
-
@classification_cache_mutex = Mutex.new
|
|
21
|
-
|
|
22
|
-
class << self
|
|
23
|
-
attr_reader :classification_cache, :classification_cache_mutex
|
|
24
|
-
end
|
|
25
|
-
|
|
26
44
|
def initialize(profile)
|
|
27
45
|
@profile = profile
|
|
28
46
|
@element_stack = [] # Track parent-child hierarchy
|
|
@@ -31,18 +49,23 @@ module SvgConform
|
|
|
31
49
|
@parse_errors = []
|
|
32
50
|
@result = nil # Will be set in end_document
|
|
33
51
|
|
|
52
|
+
# Create instance-level cache (no shared state between handlers)
|
|
53
|
+
@classification_cache = ClassificationCache.new
|
|
54
|
+
|
|
55
|
+
# Use profile requirements directly (config is stateless)
|
|
56
|
+
# Validation state lives in context, not in requirements
|
|
57
|
+
@requirements = @profile.requirements || []
|
|
58
|
+
|
|
34
59
|
# Create validation context (without document reference for SAX)
|
|
35
60
|
@context = create_sax_context
|
|
36
61
|
|
|
37
|
-
# Classify requirements into immediate vs deferred (use cache
|
|
62
|
+
# Classify requirements into immediate vs deferred (use instance cache)
|
|
38
63
|
@immediate_requirements, @deferred_requirements = classify_requirements_with_cache
|
|
39
64
|
end
|
|
40
65
|
|
|
41
66
|
# SAX Event: Document start
|
|
42
67
|
def start_document
|
|
43
|
-
#
|
|
44
|
-
reset_stateful_requirements
|
|
45
|
-
|
|
68
|
+
# No need to reset state - each validation has fresh requirement instances
|
|
46
69
|
# Initialize root level counters
|
|
47
70
|
@position_counters.push({})
|
|
48
71
|
end
|
|
@@ -158,37 +181,21 @@ module SvgConform
|
|
|
158
181
|
context.instance_variable_set(:@reference_manifest,
|
|
159
182
|
trackers[:reference_manifest])
|
|
160
183
|
context.instance_variable_set(:@fixes, [])
|
|
161
|
-
context.instance_variable_set(:@
|
|
184
|
+
context.instance_variable_set(:@state_registry, {})
|
|
162
185
|
context
|
|
163
186
|
end
|
|
164
187
|
|
|
165
|
-
# Classify requirements based on validation needs (with caching)
|
|
188
|
+
# Classify requirements based on validation needs (with instance-level caching)
|
|
166
189
|
def classify_requirements_with_cache
|
|
167
|
-
profile_key = @profile.
|
|
168
|
-
profile_requirements = @profile.requirements
|
|
169
|
-
|
|
170
|
-
# Check cache first (thread-safe)
|
|
171
|
-
@immediate_requirements = []
|
|
172
|
-
@deferred_requirements = []
|
|
190
|
+
profile_key = @profile.name || @profile.object_id.to_s
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if classified
|
|
179
|
-
@immediate_requirements = classified[:immediate].map do |req_class|
|
|
180
|
-
# Find the actual instance from profile requirements
|
|
181
|
-
profile_requirements.find { |r| r.is_a?(req_class) }
|
|
182
|
-
end.compact
|
|
183
|
-
@deferred_requirements = classified[:deferred].map do |req_class|
|
|
184
|
-
profile_requirements.find { |r| r.is_a?(req_class) }
|
|
185
|
-
end.compact
|
|
186
|
-
else
|
|
187
|
-
# Classify and cache
|
|
192
|
+
# Use instance-level cache (no mutexes needed - per-handler isolation)
|
|
193
|
+
@classification_cache.fetch(profile_key) do
|
|
194
|
+
# Classify by requirement classes
|
|
188
195
|
immediate_classes = []
|
|
189
196
|
deferred_classes = []
|
|
190
197
|
|
|
191
|
-
|
|
198
|
+
@requirements.each do |req|
|
|
192
199
|
if req.respond_to?(:needs_deferred_validation?) && req.needs_deferred_validation?
|
|
193
200
|
deferred_classes << req.class
|
|
194
201
|
else
|
|
@@ -196,45 +203,22 @@ module SvgConform
|
|
|
196
203
|
end
|
|
197
204
|
end
|
|
198
205
|
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
self.class.classification_cache[profile_key] = {
|
|
202
|
-
immediate: immediate_classes,
|
|
203
|
-
deferred: deferred_classes,
|
|
204
|
-
}
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
@immediate_requirements = profile_requirements.select do |req|
|
|
208
|
-
immediate_classes.include?(req.class)
|
|
209
|
-
end
|
|
210
|
-
@deferred_requirements = profile_requirements.select do |req|
|
|
211
|
-
deferred_classes.include?(req.class)
|
|
212
|
-
end
|
|
206
|
+
# Cache the classification by class
|
|
207
|
+
{ immediate: immediate_classes, deferred: deferred_classes }
|
|
213
208
|
end
|
|
214
209
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# Classify requirements based on validation needs
|
|
219
|
-
def classify_requirements
|
|
220
|
-
return unless @profile&.requirements
|
|
210
|
+
# Map back to actual requirement instances from @requirements
|
|
211
|
+
classified = @classification_cache.fetch(profile_key) { {} }
|
|
221
212
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
@deferred_requirements << req
|
|
225
|
-
else
|
|
226
|
-
@immediate_requirements << req
|
|
227
|
-
end
|
|
213
|
+
immediate = classified[:immediate].flat_map do |req_class|
|
|
214
|
+
@requirements.select { |r| r.is_a?(req_class) }
|
|
228
215
|
end
|
|
229
|
-
end
|
|
230
216
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return unless @profile&.requirements
|
|
234
|
-
|
|
235
|
-
@profile.requirements.each do |req|
|
|
236
|
-
req.reset_state if req.respond_to?(:reset_state)
|
|
217
|
+
deferred = classified[:deferred].flat_map do |req_class|
|
|
218
|
+
@requirements.select { |r| r.is_a?(req_class) }
|
|
237
219
|
end
|
|
220
|
+
|
|
221
|
+
[immediate, deferred]
|
|
238
222
|
end
|
|
239
223
|
end
|
|
240
224
|
end
|
|
@@ -23,18 +23,56 @@ module SvgConform
|
|
|
23
23
|
# - StructuralInvalidityTracker: tracks structurally invalid nodes
|
|
24
24
|
# - NodeIdManager: handles node ID generation for both SAX and DOM modes
|
|
25
25
|
# - ReferenceManifest: tracks ID definitions and references
|
|
26
|
+
# - State Registry: manages requirement-specific state for deferred validation
|
|
26
27
|
#
|
|
27
|
-
# ==
|
|
28
|
+
# == Validation modes
|
|
29
|
+
#
|
|
30
|
+
# === DOM validation (remediation mode)
|
|
28
31
|
#
|
|
29
|
-
# In DOM validation (remediation mode):
|
|
30
32
|
# context = ValidationContext.new(document, profile)
|
|
31
33
|
# requirement.validate_document(document, context)
|
|
32
34
|
#
|
|
33
|
-
#
|
|
35
|
+
# === SAX validation (streaming mode)
|
|
36
|
+
#
|
|
34
37
|
# context = ValidationContext.new(nil, profile)
|
|
35
38
|
# requirement.validate_sax_element(element, context)
|
|
36
39
|
#
|
|
37
|
-
# ==
|
|
40
|
+
# == State management for deferred validation
|
|
41
|
+
#
|
|
42
|
+
# Requirements that need deferred validation (collecting data during SAX parsing
|
|
43
|
+
# and validating at document end) use requirement-specific State classes:
|
|
44
|
+
#
|
|
45
|
+
# class MyRequirement < BaseRequirement
|
|
46
|
+
# class State
|
|
47
|
+
# attr_accessor :collected_items
|
|
48
|
+
#
|
|
49
|
+
# def initialize
|
|
50
|
+
# @collected_items = []
|
|
51
|
+
# end
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# def collect_sax_data(element, context)
|
|
55
|
+
# state = context.state_for(self)
|
|
56
|
+
# state.collected_items << extract_from(element)
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# def validate_sax_complete(context)
|
|
60
|
+
# state = context.state_for(self)
|
|
61
|
+
# # Validate state.collected_items
|
|
62
|
+
# end
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# The +state_for+ method:
|
|
66
|
+
# - Returns a State instance specific to the requirement class
|
|
67
|
+
# - Creates the State instance on first access (lazy initialization)
|
|
68
|
+
# - Maintains one State instance per requirement class per validation
|
|
69
|
+
# - Prevents state pollution when reusing profiles across validations
|
|
70
|
+
#
|
|
71
|
+
# Each validation gets a fresh ValidationContext with a fresh state registry,
|
|
72
|
+
# ensuring that reusing the same Profile for multiple validations doesn't leak
|
|
73
|
+
# state between validations.
|
|
74
|
+
#
|
|
75
|
+
# == Error reporting
|
|
38
76
|
#
|
|
39
77
|
# context.add_error(
|
|
40
78
|
# requirement_id: id,
|
|
@@ -44,13 +82,13 @@ module SvgConform
|
|
|
44
82
|
# fix: -> { remove_element(node) }
|
|
45
83
|
# )
|
|
46
84
|
#
|
|
47
|
-
# == Node ID
|
|
85
|
+
# == Node ID tracking
|
|
48
86
|
#
|
|
49
87
|
# The context generates unique node IDs for tracking:
|
|
50
88
|
# - SAX mode: uses pre-computed path_id from ElementProxy
|
|
51
89
|
# - DOM mode: uses DocumentAnalyzer with forward-counting algorithm
|
|
52
90
|
#
|
|
53
|
-
# == Structural
|
|
91
|
+
# == Structural invalidity
|
|
54
92
|
#
|
|
55
93
|
# Requirements can mark nodes as structurally invalid (e.g., invalid
|
|
56
94
|
# parent-child relationships). Other requirements should skip
|
|
@@ -58,7 +96,6 @@ module SvgConform
|
|
|
58
96
|
#
|
|
59
97
|
# context.mark_node_structurally_invalid(node)
|
|
60
98
|
# context.node_structurally_invalid?(node) # => true
|
|
61
|
-
#
|
|
62
99
|
class ValidationContext
|
|
63
100
|
# The document being validated (nil in SAX mode)
|
|
64
101
|
attr_reader :document
|
|
@@ -101,7 +138,13 @@ module SvgConform
|
|
|
101
138
|
@reference_manifest = trackers[:reference_manifest]
|
|
102
139
|
|
|
103
140
|
@fixes = []
|
|
104
|
-
@
|
|
141
|
+
@state_registry = {} # Maps requirement class => state instance
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get or create state for a requirement
|
|
145
|
+
# Each requirement gets its own state instance, preventing state pollution
|
|
146
|
+
def state_for(requirement)
|
|
147
|
+
@state_registry[requirement.class] ||= requirement.class::State.new
|
|
105
148
|
end
|
|
106
149
|
|
|
107
150
|
# Mark a node as structurally invalid (e.g., invalid parent-child relationship)
|
|
@@ -164,14 +207,6 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
|
|
|
164
207
|
(@error_tracker.errors + @error_tracker.warnings).count(&:fixable?)
|
|
165
208
|
end
|
|
166
209
|
|
|
167
|
-
def set_data(key, value)
|
|
168
|
-
@data[key] = value
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def get_data(key)
|
|
172
|
-
@data[key]
|
|
173
|
-
end
|
|
174
|
-
|
|
175
210
|
# Register an ID definition
|
|
176
211
|
def register_id(id_value, element_name:, line_number: nil,
|
|
177
212
|
column_number: nil)
|
data/lib/svg_conform/version.rb
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Expected errors for IETF-test.svg validation against svg_1_2_rfc profile
|
|
2
|
+
# Based on diagnostic results from script/diagnose_dom_mode.rb
|
|
3
|
+
|
|
4
|
+
description: "IETF-test.svg from metanorma-standoc - contains rgb(0,0,0) colors and style properties that should be promoted"
|
|
5
|
+
|
|
6
|
+
# Total errors expected: 18
|
|
7
|
+
total_errors: 18
|
|
8
|
+
|
|
9
|
+
# Errors by requirement
|
|
10
|
+
requirements:
|
|
11
|
+
color_restrictions:
|
|
12
|
+
total: 3
|
|
13
|
+
errors:
|
|
14
|
+
- message: "Color 'rgb(0,0,0)' in style property 'stroke' is not allowed in this profile"
|
|
15
|
+
count: 2
|
|
16
|
+
- message: "Color 'rgb(0,0,0)' in style property 'fill' is not allowed in this profile"
|
|
17
|
+
count: 1
|
|
18
|
+
|
|
19
|
+
style_promotion:
|
|
20
|
+
total: 15
|
|
21
|
+
errors:
|
|
22
|
+
- message: "Style property 'stroke' can be promoted to attribute"
|
|
23
|
+
count: 4
|
|
24
|
+
- message: "Style property 'stroke-width' can be promoted to attribute"
|
|
25
|
+
count: 2
|
|
26
|
+
- message: "Style property 'fill' can be promoted to attribute"
|
|
27
|
+
count: 6
|
|
28
|
+
- message: "Style property 'font-family' can be promoted to attribute"
|
|
29
|
+
count: 1
|
|
30
|
+
- message: "Style property 'font-size' can be promoted to attribute"
|
|
31
|
+
count: 1
|
|
32
|
+
|
|
33
|
+
# Remediation expectations
|
|
34
|
+
# Note: Remediation promotes styles to attributes (e.g., stroke="black") rather than
|
|
35
|
+
# modifying the style attribute content (e.g., style="stroke:black")
|
|
36
|
+
remediation:
|
|
37
|
+
before_errors: 18
|
|
38
|
+
after_errors: 1
|
|
39
|
+
expected_changes: 18
|
|
40
|
+
expected_remediations:
|
|
41
|
+
- color_conversion
|
|
42
|
+
- style_promotion
|
|
43
|
+
# After remediation, expect attributes like stroke="black" instead of style="stroke:rgb(0,0,0)"
|
|
44
|
+
expected_attributes:
|
|
45
|
+
- stroke: 4 # Promoted from style
|
|
46
|
+
- stroke-width: 2 # Promoted from style
|
|
47
|
+
- fill: 6 # Promoted from style (some were already fill="none")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 28000 21000">
|
|
2
|
+
<g class="Drawing" id="Straight_Connector_42">
|
|
3
|
+
<g>
|
|
4
|
+
<g style="stroke:rgb(0,0,0);stroke-width:88;fill:none">
|
|
5
|
+
<path d="M 4264,13886 L 4264,17273" style="fill:none" />
|
|
6
|
+
</g></g></g>
|
|
7
|
+
<g class="Drawing" id="Straight_Connector_33">
|
|
8
|
+
<g>
|
|
9
|
+
<g style="stroke:rgb(0,0,0);stroke-width:88;fill:none">
|
|
10
|
+
<path d="M 20355,10711 L 20351,13886" style="fill:none" />
|
|
11
|
+
</g></g></g>
|
|
12
|
+
<g class="Drawing">
|
|
13
|
+
<g>
|
|
14
|
+
<g style="stroke:none;fill:none">
|
|
15
|
+
<rect height="3490" width="25184" x="1512" y="340" />
|
|
16
|
+
</g>
|
|
17
|
+
<g style="font-family:Arial embedded;font-size:1552px;font-weight:400">
|
|
18
|
+
<g style="stroke:none;fill:rgb(0,0,0)">
|
|
19
|
+
<text>
|
|
20
|
+
<tspan x="4733 5855 6718 7582 8446 8877 9741 10605 11036 12074 12853 13716 14580 15443 15960 16307 17171 17602 18724 19071 19935 20799 21315 22179 " y="2482">
|
|
21
|
+
Updated Scenario Diagram
|
|
22
|
+
</tspan>
|
|
23
|
+
</text>
|
|
24
|
+
</g>
|
|
25
|
+
</g>
|
|
26
|
+
</g>
|
|
27
|
+
</g>
|
|
28
|
+
</svg>
|
data/spec/spec_helper.rb
CHANGED
|
@@ -24,11 +24,6 @@ RSpec.configure do |config|
|
|
|
24
24
|
SvgConform::Requirements::AllowedElementsRequirement.configuration_validation_cache.clear
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Clear SaxValidationHandler classification cache
|
|
28
|
-
if defined?(SvgConform::SaxValidationHandler)
|
|
29
|
-
SvgConform::SaxValidationHandler.classification_cache.clear
|
|
30
|
-
end
|
|
31
|
-
|
|
32
27
|
# Clear ProfileCompiler compile cache
|
|
33
28
|
if defined?(SvgConform::ProfileCompiler)
|
|
34
29
|
SvgConform::ProfileCompiler.compile_cache.clear
|
|
@@ -8,6 +8,26 @@ RSpec.describe "SVG 1.2 RFC Profile" do
|
|
|
8
8
|
let(:svg_1_2_rfc_profile_path) { "config/profiles/svg_1_2_rfc.yml" }
|
|
9
9
|
let(:fixtures_base_dir) { "spec/fixtures" }
|
|
10
10
|
|
|
11
|
+
# Helper module for grouping and analyzing errors (namespaced to avoid global pollution)
|
|
12
|
+
module SpecHelpers
|
|
13
|
+
def self.group_errors_by_requirement(validation_result)
|
|
14
|
+
validation_result.errors.each_with_object({}) do |error, hash|
|
|
15
|
+
req_id = error.requirement_id || "unknown"
|
|
16
|
+
hash[req_id] ||= []
|
|
17
|
+
hash[req_id] << error
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.count_style_properties(style_errors)
|
|
22
|
+
style_errors.each_with_object({}) do |error, hash|
|
|
23
|
+
if error.message =~ /Style property '([-\w]+)' can be promoted/
|
|
24
|
+
hash[$1] ||= 0
|
|
25
|
+
hash[$1] += 1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
11
31
|
describe "profile loading" do
|
|
12
32
|
it "loads the SVG 1.2 RFC profile successfully" do
|
|
13
33
|
profile = SvgConform::Profile.load_from_file(svg_1_2_rfc_profile_path)
|
|
@@ -197,4 +217,134 @@ RSpec.describe "SVG 1.2 RFC Profile" do
|
|
|
197
217
|
puts "Comprehensive test: #{initial_result.errors.count} → #{final_result.errors.count} violations"
|
|
198
218
|
end
|
|
199
219
|
end
|
|
220
|
+
|
|
221
|
+
describe "IETF-test.svg (issue #75)" do
|
|
222
|
+
let(:ietf_test_file) do
|
|
223
|
+
"spec/fixtures/svg_1_2_rfc/inputs/ietf_test_violations.svg"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
before do
|
|
227
|
+
skip "IETF-test fixture not found" unless File.exist?(ietf_test_file)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "validates IETF-test.svg and detects expected violations" do
|
|
231
|
+
document = SvgConform::Document.from_file(ietf_test_file)
|
|
232
|
+
profile = SvgConform::Profile.load_from_file(svg_1_2_rfc_profile_path)
|
|
233
|
+
validation_result = profile.validate(document)
|
|
234
|
+
errors_by_requirement = SpecHelpers.group_errors_by_requirement(validation_result)
|
|
235
|
+
|
|
236
|
+
# Should detect violations
|
|
237
|
+
expect(validation_result.valid?).to be false
|
|
238
|
+
|
|
239
|
+
# Should have exactly 18 errors (3 color + 15 style promotion)
|
|
240
|
+
expect(validation_result.errors.count).to eq(18)
|
|
241
|
+
|
|
242
|
+
# Check color_restrictions errors
|
|
243
|
+
color_errors = errors_by_requirement["color_restrictions"] || []
|
|
244
|
+
expect(color_errors.count).to eq(3)
|
|
245
|
+
|
|
246
|
+
# Check style_promotion errors
|
|
247
|
+
style_errors = errors_by_requirement["style_promotion"] || []
|
|
248
|
+
expect(style_errors.count).to eq(15)
|
|
249
|
+
|
|
250
|
+
puts "IETF-test.svg validation: #{validation_result.errors.count} errors detected"
|
|
251
|
+
puts " Color errors: #{color_errors.count}"
|
|
252
|
+
puts " Style promotion errors: #{style_errors.count}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "validates IETF-test.svg color violations in detail" do
|
|
256
|
+
document = SvgConform::Document.from_file(ietf_test_file)
|
|
257
|
+
profile = SvgConform::Profile.load_from_file(svg_1_2_rfc_profile_path)
|
|
258
|
+
validation_result = profile.validate(document)
|
|
259
|
+
errors_by_requirement = SpecHelpers.group_errors_by_requirement(validation_result)
|
|
260
|
+
color_errors = errors_by_requirement["color_restrictions"] || []
|
|
261
|
+
|
|
262
|
+
color_stroke_errors = color_errors.select do |e|
|
|
263
|
+
e.message.include?("stroke")
|
|
264
|
+
end
|
|
265
|
+
color_fill_errors = color_errors.select { |e| e.message.include?("fill") }
|
|
266
|
+
|
|
267
|
+
expect(color_stroke_errors.count).to eq(2)
|
|
268
|
+
expect(color_fill_errors.count).to eq(1)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "validates IETF-test.svg style promotion violations in detail" do
|
|
272
|
+
document = SvgConform::Document.from_file(ietf_test_file)
|
|
273
|
+
profile = SvgConform::Profile.load_from_file(svg_1_2_rfc_profile_path)
|
|
274
|
+
validation_result = profile.validate(document)
|
|
275
|
+
errors_by_requirement = SpecHelpers.group_errors_by_requirement(validation_result)
|
|
276
|
+
style_errors = errors_by_requirement["style_promotion"] || []
|
|
277
|
+
style_props = SpecHelpers.count_style_properties(style_errors)
|
|
278
|
+
|
|
279
|
+
expect(style_props["stroke"]).to eq(4)
|
|
280
|
+
expect(style_props["stroke-width"]).to eq(2)
|
|
281
|
+
expect(style_props["fill"]).to eq(6)
|
|
282
|
+
expect(style_props["font-family"]).to eq(1)
|
|
283
|
+
expect(style_props["font-size"]).to eq(1)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it "applies remediations to IETF-test.svg and reduces errors" do
|
|
287
|
+
document = SvgConform::Document.from_file(ietf_test_file)
|
|
288
|
+
profile = SvgConform::Profile.load_from_file(svg_1_2_rfc_profile_path)
|
|
289
|
+
initial_result = profile.validate(document)
|
|
290
|
+
initial_error_count = initial_result.errors.count
|
|
291
|
+
|
|
292
|
+
# Apply all remediations
|
|
293
|
+
changes = profile.apply_remediations(document)
|
|
294
|
+
|
|
295
|
+
# Get final error count
|
|
296
|
+
final_result = profile.validate(document)
|
|
297
|
+
final_error_count = final_result.errors.count
|
|
298
|
+
|
|
299
|
+
# Verify remediation was applied
|
|
300
|
+
expect(changes.count).to be > 0
|
|
301
|
+
|
|
302
|
+
# Verify errors were reduced
|
|
303
|
+
expect(final_error_count).to be < initial_error_count
|
|
304
|
+
|
|
305
|
+
# Verify specific error reduction
|
|
306
|
+
expect(initial_error_count).to eq(18)
|
|
307
|
+
expect(final_error_count).to eq(1)
|
|
308
|
+
|
|
309
|
+
# Verify specific remediations were applied
|
|
310
|
+
color_changes = changes.select do |c|
|
|
311
|
+
c[:type] == :attribute_modified || c[:message]&.include?("color")
|
|
312
|
+
end
|
|
313
|
+
style_promotion_changes = changes.select do |c|
|
|
314
|
+
c[:type] == :style_promotion || c[:message]&.include?("promoted")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
expect(color_changes.count).to be > 0
|
|
318
|
+
expect(style_promotion_changes.count).to be > 0
|
|
319
|
+
|
|
320
|
+
puts "IETF-test.svg remediation: #{initial_error_count} → #{final_error_count} errors"
|
|
321
|
+
puts " Total changes: #{changes.count}"
|
|
322
|
+
puts " - Color conversion: #{color_changes.count}"
|
|
323
|
+
puts " - Style promotion: #{style_promotion_changes.count}"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "produces valid XML after remediation" do
|
|
327
|
+
document = SvgConform::Document.from_file(ietf_test_file)
|
|
328
|
+
profile = SvgConform::Profile.load_from_file(svg_1_2_rfc_profile_path)
|
|
329
|
+
|
|
330
|
+
# Apply remediations
|
|
331
|
+
profile.apply_remediations(document)
|
|
332
|
+
|
|
333
|
+
# Should still be valid XML
|
|
334
|
+
xml = document.to_xml
|
|
335
|
+
expect(xml).to match(/<svg\s+[^>]*>.*<\/svg>/m)
|
|
336
|
+
|
|
337
|
+
# Should maintain root element structure
|
|
338
|
+
expect(document.root.name).to eq("svg")
|
|
339
|
+
|
|
340
|
+
# Check for promoted stroke attributes (from style promotion)
|
|
341
|
+
expect(xml).to include('stroke="black"')
|
|
342
|
+
|
|
343
|
+
# Check for stroke-width attributes (from style promotion)
|
|
344
|
+
expect(xml).to include('stroke-width="88"')
|
|
345
|
+
|
|
346
|
+
# Check for fill attributes (from both color conversion and style promotion)
|
|
347
|
+
expect(xml).to include('fill="black"').or(include('fill="none"'))
|
|
348
|
+
end
|
|
349
|
+
end
|
|
200
350
|
end
|