svg_conform 0.1.2 → 0.1.3
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 +192 -32
- data/README.adoc +55 -0
- data/lib/svg_conform/element_proxy.rb +10 -10
- data/lib/svg_conform/fast_document_analyzer.rb +22 -22
- data/lib/svg_conform/node_index_builder.rb +1 -0
- data/lib/svg_conform/requirements/allowed_elements_requirement.rb +29 -9
- data/lib/svg_conform/requirements/color_restrictions_requirement.rb +8 -6
- data/lib/svg_conform/requirements/font_family_requirement.rb +6 -2
- data/lib/svg_conform/requirements/forbidden_content_requirement.rb +2 -2
- data/lib/svg_conform/requirements/id_reference_requirement.rb +10 -7
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +7 -6
- data/lib/svg_conform/requirements/link_validation_requirement.rb +2 -2
- data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +16 -10
- data/lib/svg_conform/requirements/namespace_requirement.rb +11 -10
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +4 -4
- data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +2 -2
- data/lib/svg_conform/requirements/no_external_images_requirement.rb +2 -2
- data/lib/svg_conform/requirements/style_promotion_requirement.rb +7 -0
- data/lib/svg_conform/requirements/viewbox_required_requirement.rb +7 -7
- data/lib/svg_conform/sax_document.rb +2 -2
- data/lib/svg_conform/sax_validation_handler.rb +14 -10
- data/lib/svg_conform/validation_context.rb +9 -7
- data/lib/svg_conform/validator.rb +2 -2
- data/lib/svg_conform/version.rb +1 -1
- data/spec/spec_helper.rb +3 -0
- data/spec/support/shared_examples_for_validation_modes.rb +71 -0
- metadata +3 -2
|
@@ -121,6 +121,13 @@ module SvgConform
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def validate_sax_element(element, context)
|
|
124
|
+
# Skip if parent is structurally invalid (matches DOM behavior)
|
|
125
|
+
if element.parent && context.node_structurally_invalid?(element.parent)
|
|
126
|
+
# Mark this element as invalid too since it won't be in final document
|
|
127
|
+
context.mark_node_structurally_invalid(element)
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
124
131
|
# Skip foreign namespace elements if configured (let NamespaceRequirement handle them)
|
|
125
132
|
if skip_foreign_namespaces && foreign_namespace_sax?(element)
|
|
126
133
|
return
|
|
@@ -135,7 +142,7 @@ module SvgConform
|
|
|
135
142
|
message: "Element '#{element_name}' is not allowed in this profile",
|
|
136
143
|
node: element,
|
|
137
144
|
severity: :error,
|
|
138
|
-
data: { element: element_name }
|
|
145
|
+
data: { element: element_name },
|
|
139
146
|
)
|
|
140
147
|
return
|
|
141
148
|
end
|
|
@@ -149,7 +156,7 @@ module SvgConform
|
|
|
149
156
|
message: "The element '#{element_name}' is not allowed as a child of '#{parent_name}'",
|
|
150
157
|
node: element,
|
|
151
158
|
severity: :error,
|
|
152
|
-
data: { element: element_name, parent: parent_name }
|
|
159
|
+
data: { element: element_name, parent: parent_name },
|
|
153
160
|
)
|
|
154
161
|
# Mark node as structurally invalid
|
|
155
162
|
context.mark_node_structurally_invalid(element)
|
|
@@ -166,7 +173,7 @@ module SvgConform
|
|
|
166
173
|
message: "Element '#{element_name}' is not allowed in this profile",
|
|
167
174
|
node: element,
|
|
168
175
|
severity: :error,
|
|
169
|
-
data: { element: element_name }
|
|
176
|
+
data: { element: element_name },
|
|
170
177
|
)
|
|
171
178
|
# Mark as structurally invalid
|
|
172
179
|
context.mark_node_structurally_invalid(element)
|
|
@@ -184,7 +191,7 @@ module SvgConform
|
|
|
184
191
|
requirement_id: id,
|
|
185
192
|
message: error[:message],
|
|
186
193
|
node: element,
|
|
187
|
-
severity: :error
|
|
194
|
+
severity: :error,
|
|
188
195
|
)
|
|
189
196
|
end
|
|
190
197
|
end
|
|
@@ -339,7 +346,13 @@ module SvgConform
|
|
|
339
346
|
def foreign_namespace_sax?(element)
|
|
340
347
|
return false unless skip_foreign_namespaces
|
|
341
348
|
|
|
342
|
-
# Check if element has a namespace
|
|
349
|
+
# Check if element name has a namespace prefix (e.g., rdf:RDF, cc:Work)
|
|
350
|
+
if element.name.include?(":")
|
|
351
|
+
# Element has prefix, it's in a foreign namespace
|
|
352
|
+
return true
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Also check xmlns attribute
|
|
343
356
|
element_namespace = element.namespace
|
|
344
357
|
|
|
345
358
|
# No namespace or empty namespace means SVG namespace (default)
|
|
@@ -374,7 +387,9 @@ module SvgConform
|
|
|
374
387
|
|
|
375
388
|
return errors unless element_configs&.any?
|
|
376
389
|
|
|
377
|
-
element_config = element_configs.find
|
|
390
|
+
element_config = element_configs.find do |config|
|
|
391
|
+
config.tag == element_name
|
|
392
|
+
end
|
|
378
393
|
return errors unless element_config&.attr
|
|
379
394
|
|
|
380
395
|
allowed_attrs = []
|
|
@@ -414,6 +429,11 @@ module SvgConform
|
|
|
414
429
|
attr_name = attr.name.downcase
|
|
415
430
|
next if attr_name.start_with?("xmlns:")
|
|
416
431
|
next if attr_name.start_with?("xml:")
|
|
432
|
+
|
|
433
|
+
# Skip namespaced attributes (those with colon prefix like inkscape:label)
|
|
434
|
+
# This matches DOM behavior where XPath skips attributes with namespaces
|
|
435
|
+
next if attr_name.include?(":") && !attr_name.start_with?("xmlns:")
|
|
436
|
+
|
|
417
437
|
next if attr.namespace
|
|
418
438
|
next if attr_name.start_with?("data-")
|
|
419
439
|
|
|
@@ -422,7 +442,7 @@ module SvgConform
|
|
|
422
442
|
errors << {
|
|
423
443
|
type: :explicitly_disallowed,
|
|
424
444
|
attribute: attr_name,
|
|
425
|
-
message: "Attribute '#{attr_name}' is explicitly disallowed on element '#{element_name}'"
|
|
445
|
+
message: "Attribute '#{attr_name}' is explicitly disallowed on element '#{element_name}'",
|
|
426
446
|
}
|
|
427
447
|
next
|
|
428
448
|
end
|
|
@@ -433,7 +453,7 @@ module SvgConform
|
|
|
433
453
|
errors << {
|
|
434
454
|
type: :not_allowed,
|
|
435
455
|
attribute: attr_name,
|
|
436
|
-
message: "Attribute '#{attr_name}' is not allowed on element '#{element_name}'"
|
|
456
|
+
message: "Attribute '#{attr_name}' is not allowed on element '#{element_name}'",
|
|
437
457
|
}
|
|
438
458
|
end
|
|
439
459
|
|
|
@@ -462,7 +482,7 @@ module SvgConform
|
|
|
462
482
|
errors << {
|
|
463
483
|
type: :globally_disallowed,
|
|
464
484
|
attribute: attr_name,
|
|
465
|
-
message: "Attribute '#{attr_name}' is globally disallowed in this profile"
|
|
485
|
+
message: "Attribute '#{attr_name}' is globally disallowed in this profile",
|
|
466
486
|
}
|
|
467
487
|
end
|
|
468
488
|
|
|
@@ -85,7 +85,8 @@ module SvgConform
|
|
|
85
85
|
return if context.node_structurally_invalid?(element)
|
|
86
86
|
|
|
87
87
|
# Check color-related attributes
|
|
88
|
-
color_attributes = %w[fill stroke color stop-color flood-color
|
|
88
|
+
color_attributes = %w[fill stroke color stop-color flood-color
|
|
89
|
+
lighting-color]
|
|
89
90
|
|
|
90
91
|
color_attributes.each do |attr_name|
|
|
91
92
|
value = element.raw_attributes[attr_name]
|
|
@@ -101,8 +102,8 @@ module SvgConform
|
|
|
101
102
|
data: {
|
|
102
103
|
attribute: attr_name,
|
|
103
104
|
value: value,
|
|
104
|
-
element: element.name
|
|
105
|
-
}
|
|
105
|
+
element: element.name,
|
|
106
|
+
},
|
|
106
107
|
)
|
|
107
108
|
end
|
|
108
109
|
|
|
@@ -111,7 +112,8 @@ module SvgConform
|
|
|
111
112
|
return unless style_value
|
|
112
113
|
|
|
113
114
|
styles = parse_style(style_value)
|
|
114
|
-
color_properties = %w[fill stroke color stop-color flood-color
|
|
115
|
+
color_properties = %w[fill stroke color stop-color flood-color
|
|
116
|
+
lighting-color]
|
|
115
117
|
|
|
116
118
|
color_properties.each do |prop|
|
|
117
119
|
value = styles[prop]
|
|
@@ -127,8 +129,8 @@ module SvgConform
|
|
|
127
129
|
data: {
|
|
128
130
|
attribute: prop,
|
|
129
131
|
value: value,
|
|
130
|
-
element: element.name
|
|
131
|
-
}
|
|
132
|
+
element: element.name,
|
|
133
|
+
},
|
|
132
134
|
)
|
|
133
135
|
end
|
|
134
136
|
end
|
|
@@ -47,19 +47,23 @@ module SvgConform
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def validate_sax_element(element, context)
|
|
50
|
+
# Skip if parent is structurally invalid
|
|
51
|
+
return if element.parent && context.node_structurally_invalid?(element.parent)
|
|
52
|
+
|
|
50
53
|
# Check font-family attribute only
|
|
51
54
|
font_family = element.raw_attributes["font-family"]
|
|
52
55
|
return unless font_family
|
|
53
56
|
|
|
54
57
|
if svgcheck_compatibility
|
|
55
|
-
check_font_family_svgcheck_mode(element, context, font_family,
|
|
58
|
+
check_font_family_svgcheck_mode(element, context, font_family,
|
|
59
|
+
"font-family")
|
|
56
60
|
elsif !valid_font_family?(font_family)
|
|
57
61
|
context.add_error(
|
|
58
62
|
requirement_id: id,
|
|
59
63
|
message: "Font family '#{font_family}' is not allowed in this profile",
|
|
60
64
|
node: element,
|
|
61
65
|
severity: :error,
|
|
62
|
-
data: { attribute: "font-family", value: font_family }
|
|
66
|
+
data: { attribute: "font-family", value: font_family },
|
|
63
67
|
)
|
|
64
68
|
end
|
|
65
69
|
end
|
|
@@ -63,7 +63,7 @@ module SvgConform
|
|
|
63
63
|
requirement_id: id,
|
|
64
64
|
message: "Forbidden element '#{element.name}' is not allowed",
|
|
65
65
|
node: element,
|
|
66
|
-
severity: :error
|
|
66
|
+
severity: :error,
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -76,7 +76,7 @@ module SvgConform
|
|
|
76
76
|
requirement_id: id,
|
|
77
77
|
message: "Forbidden attribute '#{attr_name}' is not allowed",
|
|
78
78
|
node: element,
|
|
79
|
-
severity: :error
|
|
79
|
+
severity: :error,
|
|
80
80
|
)
|
|
81
81
|
end
|
|
82
82
|
end
|
|
@@ -10,7 +10,7 @@ module SvgConform
|
|
|
10
10
|
true
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def collect_sax_data(element,
|
|
13
|
+
def collect_sax_data(element, _context)
|
|
14
14
|
# Initialize collections on first call
|
|
15
15
|
@collected_ids ||= Set.new
|
|
16
16
|
@collected_url_refs ||= []
|
|
@@ -22,7 +22,8 @@ module SvgConform
|
|
|
22
22
|
@collected_ids.add(id_value) if id_value && !id_value.empty?
|
|
23
23
|
|
|
24
24
|
# Collect url() references
|
|
25
|
-
url_attributes = %w[fill stroke marker-start marker-mid marker-end
|
|
25
|
+
url_attributes = %w[fill stroke marker-start marker-mid marker-end
|
|
26
|
+
clip-path mask filter]
|
|
26
27
|
url_attributes.each do |attr_name|
|
|
27
28
|
attr_value = element.raw_attributes[attr_name]
|
|
28
29
|
next unless attr_value
|
|
@@ -45,12 +46,13 @@ module SvgConform
|
|
|
45
46
|
# Collect href references
|
|
46
47
|
href_value = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
|
|
47
48
|
if href_value&.start_with?("#")
|
|
48
|
-
ref_id = href_value[1..]
|
|
49
|
+
ref_id = href_value[1..] # Remove #
|
|
49
50
|
@collected_href_refs << [element, ref_id]
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
# Collect other ID references
|
|
53
|
-
id_ref_attributes = %w[for aria-labelledby aria-describedby
|
|
54
|
+
id_ref_attributes = %w[for aria-labelledby aria-describedby
|
|
55
|
+
aria-controls aria-owns]
|
|
54
56
|
id_ref_attributes.each do |attr_name|
|
|
55
57
|
attr_value = element.raw_attributes[attr_name]
|
|
56
58
|
next unless attr_value
|
|
@@ -58,6 +60,7 @@ module SvgConform
|
|
|
58
60
|
ref_ids = attr_value.split(/\s+/)
|
|
59
61
|
ref_ids.each do |ref_id|
|
|
60
62
|
next if ref_id.empty?
|
|
63
|
+
|
|
61
64
|
@collected_other_refs << [element, ref_id, attr_name]
|
|
62
65
|
end
|
|
63
66
|
end
|
|
@@ -77,7 +80,7 @@ module SvgConform
|
|
|
77
80
|
context.add_error(
|
|
78
81
|
node: element,
|
|
79
82
|
message: message,
|
|
80
|
-
requirement_id: id
|
|
83
|
+
requirement_id: id,
|
|
81
84
|
)
|
|
82
85
|
end
|
|
83
86
|
|
|
@@ -87,7 +90,7 @@ module SvgConform
|
|
|
87
90
|
context.add_error(
|
|
88
91
|
node: element,
|
|
89
92
|
message: "Reference to undefined ID '#{ref_id}' in href attribute",
|
|
90
|
-
requirement_id: id
|
|
93
|
+
requirement_id: id,
|
|
91
94
|
)
|
|
92
95
|
end
|
|
93
96
|
|
|
@@ -97,7 +100,7 @@ module SvgConform
|
|
|
97
100
|
context.add_error(
|
|
98
101
|
node: element,
|
|
99
102
|
message: "Reference to undefined ID '#{ref_id}' in #{attr_name} attribute",
|
|
100
|
-
requirement_id: id
|
|
103
|
+
requirement_id: id,
|
|
101
104
|
)
|
|
102
105
|
end
|
|
103
106
|
end
|
|
@@ -24,15 +24,15 @@ module SvgConform
|
|
|
24
24
|
def initialize(*args)
|
|
25
25
|
super
|
|
26
26
|
@collected_ids = Set.new
|
|
27
|
-
@use_element_refs = []
|
|
28
|
-
@other_refs = []
|
|
27
|
+
@use_element_refs = [] # [element, ref_id, href]
|
|
28
|
+
@other_refs = [] # [element, ref_id, attr_name, value]
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def needs_deferred_validation?
|
|
32
32
|
true
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def collect_sax_data(element,
|
|
35
|
+
def collect_sax_data(element, _context)
|
|
36
36
|
# Initialize collections on first call
|
|
37
37
|
@collected_ids ||= Set.new
|
|
38
38
|
@use_element_refs ||= []
|
|
@@ -53,7 +53,8 @@ module SvgConform
|
|
|
53
53
|
|
|
54
54
|
# Collect other ID references if enabled
|
|
55
55
|
if check_other_references
|
|
56
|
-
id_reference_attributes = %w[clip-path mask filter marker-start
|
|
56
|
+
id_reference_attributes = %w[clip-path mask filter marker-start
|
|
57
|
+
marker-mid marker-end fill stroke]
|
|
57
58
|
|
|
58
59
|
id_reference_attributes.each do |attr_name|
|
|
59
60
|
attr_value = element.raw_attributes[attr_name]
|
|
@@ -87,7 +88,7 @@ module SvgConform
|
|
|
87
88
|
node: element,
|
|
88
89
|
message: "use element references non-existent ID: #{ref_id}",
|
|
89
90
|
severity: :error,
|
|
90
|
-
data: { invalid_id: ref_id, href: href }
|
|
91
|
+
data: { invalid_id: ref_id, href: href },
|
|
91
92
|
)
|
|
92
93
|
end
|
|
93
94
|
|
|
@@ -107,7 +108,7 @@ module SvgConform
|
|
|
107
108
|
node: element,
|
|
108
109
|
message: message,
|
|
109
110
|
severity: :error,
|
|
110
|
-
data: { invalid_id: ref_id, attribute: attr_name, value: value }
|
|
111
|
+
data: { invalid_id: ref_id, attribute: attr_name, value: value },
|
|
111
112
|
)
|
|
112
113
|
end
|
|
113
114
|
end
|
|
@@ -54,7 +54,7 @@ module SvgConform
|
|
|
54
54
|
requirement_id: id,
|
|
55
55
|
message: "Link href '#{href_value}' contains non-ASCII characters",
|
|
56
56
|
node: element,
|
|
57
|
-
severity: :error
|
|
57
|
+
severity: :error,
|
|
58
58
|
)
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -70,7 +70,7 @@ module SvgConform
|
|
|
70
70
|
requirement_id: id,
|
|
71
71
|
message: "IRI attribute '#{attr_name}' value '#{iri_value}' contains non-ASCII characters",
|
|
72
72
|
node: element,
|
|
73
|
-
severity: :error
|
|
73
|
+
severity: :error,
|
|
74
74
|
)
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -42,8 +42,16 @@ module SvgConform
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def validate_sax_element(element, context)
|
|
45
|
+
# Extract local name without prefix for exemption check
|
|
46
|
+
local_name = if element.name.include?(":")
|
|
47
|
+
element.name.split(":",
|
|
48
|
+
2).last
|
|
49
|
+
else
|
|
50
|
+
element.name
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
# Skip validation for exempt elements (e.g., RDF metadata elements)
|
|
46
|
-
return if exempt_elements.include?(element.name)
|
|
54
|
+
return if exempt_elements.include?(local_name) || exempt_elements.include?(element.name)
|
|
47
55
|
|
|
48
56
|
# Check all attributes for namespace violations
|
|
49
57
|
element.attributes.each do |attr|
|
|
@@ -67,12 +75,12 @@ module SvgConform
|
|
|
67
75
|
|
|
68
76
|
# Determine if this namespace is invalid based on configuration
|
|
69
77
|
invalid_namespace = if allowed_namespaces.empty?
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
# Blacklist mode: disallowed namespaces are forbidden
|
|
79
|
+
disallowed_namespaces.include?(namespace_uri)
|
|
80
|
+
else
|
|
81
|
+
# Whitelist mode: only allowed namespaces are permitted
|
|
82
|
+
!allowed_namespaces.include?(namespace_uri)
|
|
83
|
+
end
|
|
76
84
|
|
|
77
85
|
return unless invalid_namespace
|
|
78
86
|
|
|
@@ -81,7 +89,7 @@ module SvgConform
|
|
|
81
89
|
message: "Element '#{element.name}' does not allow attributes with namespace '#{namespace_uri}'",
|
|
82
90
|
node: element,
|
|
83
91
|
severity: :error,
|
|
84
|
-
data: { attribute: attr_name, namespace: namespace_uri }
|
|
92
|
+
data: { attribute: attr_name, namespace: namespace_uri },
|
|
85
93
|
)
|
|
86
94
|
end
|
|
87
95
|
|
|
@@ -100,8 +108,6 @@ module SvgConform
|
|
|
100
108
|
nil
|
|
101
109
|
end
|
|
102
110
|
|
|
103
|
-
private
|
|
104
|
-
|
|
105
111
|
def check_attribute_nodes(node, context)
|
|
106
112
|
node.attribute_nodes.each do |attr|
|
|
107
113
|
# Check if attribute has a namespace
|
|
@@ -213,8 +213,8 @@ module SvgConform
|
|
|
213
213
|
data: {
|
|
214
214
|
element_name: element.name,
|
|
215
215
|
namespace: element_namespace,
|
|
216
|
-
allowed_namespaces: effective_allowed_namespaces
|
|
217
|
-
}
|
|
216
|
+
allowed_namespaces: effective_allowed_namespaces,
|
|
217
|
+
},
|
|
218
218
|
)
|
|
219
219
|
return
|
|
220
220
|
end
|
|
@@ -230,24 +230,25 @@ module SvgConform
|
|
|
230
230
|
data: {
|
|
231
231
|
element_name: element.name,
|
|
232
232
|
namespace: element_namespace,
|
|
233
|
-
disallowed_namespaces: disallowed_namespaces
|
|
234
|
-
}
|
|
233
|
+
disallowed_namespaces: disallowed_namespaces,
|
|
234
|
+
},
|
|
235
235
|
)
|
|
236
236
|
end
|
|
237
237
|
|
|
238
238
|
private
|
|
239
239
|
|
|
240
240
|
def get_element_namespace_sax(element)
|
|
241
|
-
#
|
|
242
|
-
namespace = element.namespace
|
|
243
|
-
return namespace if namespace && !namespace.empty?
|
|
244
|
-
|
|
245
|
-
# If no namespace found, check if element has a prefix (indicating it's namespaced)
|
|
241
|
+
# Check if element name has a namespace prefix (e.g., rdf:RDF, cc:Work)
|
|
246
242
|
if element.name.include?(":")
|
|
247
243
|
prefix = element.name.split(":").first
|
|
248
|
-
|
|
244
|
+
namespace_uri = find_namespace_uri_for_prefix_sax(element, prefix)
|
|
245
|
+
return namespace_uri if namespace_uri
|
|
249
246
|
end
|
|
250
247
|
|
|
248
|
+
# Try to get namespace from xmlns attribute
|
|
249
|
+
namespace = element.namespace
|
|
250
|
+
return namespace if namespace && !namespace.empty?
|
|
251
|
+
|
|
251
252
|
nil
|
|
252
253
|
end
|
|
253
254
|
|
|
@@ -131,7 +131,7 @@ module SvgConform
|
|
|
131
131
|
requirement_id: id,
|
|
132
132
|
message: "External CSS import not allowed: #{url}",
|
|
133
133
|
node: element,
|
|
134
|
-
severity: :error
|
|
134
|
+
severity: :error,
|
|
135
135
|
)
|
|
136
136
|
end
|
|
137
137
|
end
|
|
@@ -145,7 +145,7 @@ module SvgConform
|
|
|
145
145
|
requirement_id: id,
|
|
146
146
|
message: "External CSS import not allowed: #{url}",
|
|
147
147
|
node: element,
|
|
148
|
-
severity: :error
|
|
148
|
+
severity: :error,
|
|
149
149
|
)
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -161,7 +161,7 @@ module SvgConform
|
|
|
161
161
|
requirement_id: id,
|
|
162
162
|
message: "External CSS link not allowed: #{href}",
|
|
163
163
|
node: element,
|
|
164
|
-
severity: :error
|
|
164
|
+
severity: :error,
|
|
165
165
|
)
|
|
166
166
|
end
|
|
167
167
|
|
|
@@ -179,7 +179,7 @@ module SvgConform
|
|
|
179
179
|
requirement_id: id,
|
|
180
180
|
message: "External URL reference in style attribute not allowed: #{url}",
|
|
181
181
|
node: element,
|
|
182
|
-
severity: :error
|
|
182
|
+
severity: :error,
|
|
183
183
|
)
|
|
184
184
|
end
|
|
185
185
|
|
|
@@ -127,7 +127,7 @@ module SvgConform
|
|
|
127
127
|
requirement_id: id,
|
|
128
128
|
message: "External font reference not allowed: #{url}. Fonts must be embedded as data URIs.",
|
|
129
129
|
node: element,
|
|
130
|
-
severity: :error
|
|
130
|
+
severity: :error,
|
|
131
131
|
)
|
|
132
132
|
end
|
|
133
133
|
end
|
|
@@ -154,7 +154,7 @@ module SvgConform
|
|
|
154
154
|
requirement_id: id,
|
|
155
155
|
message: "External font URL in style attribute not allowed: #{url}. Fonts must be embedded as data URIs.",
|
|
156
156
|
node: element,
|
|
157
|
-
severity: :error
|
|
157
|
+
severity: :error,
|
|
158
158
|
)
|
|
159
159
|
end
|
|
160
160
|
|
|
@@ -88,7 +88,7 @@ module SvgConform
|
|
|
88
88
|
requirement_id: id,
|
|
89
89
|
message: "External image reference not allowed: #{href}. Images must be embedded as data URIs.",
|
|
90
90
|
node: element,
|
|
91
|
-
severity: :error
|
|
91
|
+
severity: :error,
|
|
92
92
|
)
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -105,7 +105,7 @@ module SvgConform
|
|
|
105
105
|
requirement_id: id,
|
|
106
106
|
message: "External image URL in style attribute not allowed: #{url}. Images must be embedded as data URIs.",
|
|
107
107
|
node: element,
|
|
108
|
-
severity: :error
|
|
108
|
+
severity: :error,
|
|
109
109
|
)
|
|
110
110
|
end
|
|
111
111
|
end
|
|
@@ -20,6 +20,13 @@ module SvgConform
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def validate_sax_element(element, context)
|
|
24
|
+
style_attr = element.raw_attributes["style"]
|
|
25
|
+
return if style_attr.nil? || style_attr.strip.empty?
|
|
26
|
+
|
|
27
|
+
validate_style_properties(element, style_attr, context)
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
private
|
|
24
31
|
|
|
25
32
|
def validate_style_properties(element, style_attr, context)
|
|
@@ -95,7 +95,7 @@ module SvgConform
|
|
|
95
95
|
message: "SVG root element must have a viewBox attribute",
|
|
96
96
|
node: element,
|
|
97
97
|
severity: :error,
|
|
98
|
-
data: { missing_attribute: "viewBox" }
|
|
98
|
+
data: { missing_attribute: "viewBox" },
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
# Add informational message about calculated viewBox if width/height are present
|
|
@@ -112,8 +112,8 @@ module SvgConform
|
|
|
112
112
|
data: {
|
|
113
113
|
calculated_viewbox: calculated_viewbox,
|
|
114
114
|
source_width: width,
|
|
115
|
-
source_height: height
|
|
116
|
-
}
|
|
115
|
+
source_height: height,
|
|
116
|
+
},
|
|
117
117
|
)
|
|
118
118
|
end
|
|
119
119
|
return
|
|
@@ -130,8 +130,8 @@ module SvgConform
|
|
|
130
130
|
severity: :error,
|
|
131
131
|
data: {
|
|
132
132
|
viewbox_value: viewbox,
|
|
133
|
-
parsed_parts: parts
|
|
134
|
-
}
|
|
133
|
+
parsed_parts: parts,
|
|
134
|
+
},
|
|
135
135
|
)
|
|
136
136
|
return
|
|
137
137
|
end
|
|
@@ -150,8 +150,8 @@ module SvgConform
|
|
|
150
150
|
data: {
|
|
151
151
|
viewbox_value: viewbox,
|
|
152
152
|
width: width,
|
|
153
|
-
height: height
|
|
154
|
-
}
|
|
153
|
+
height: height,
|
|
154
|
+
},
|
|
155
155
|
)
|
|
156
156
|
end
|
|
157
157
|
|
|
@@ -27,14 +27,14 @@ module SvgConform
|
|
|
27
27
|
def validate_with_profile(profile)
|
|
28
28
|
handler = SaxValidationHandler.new(profile)
|
|
29
29
|
parser = Nokogiri::XML::SAX::Parser.new(handler)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
begin
|
|
32
32
|
parser.parse(@content)
|
|
33
33
|
rescue StandardError => e
|
|
34
34
|
# Handle parse errors
|
|
35
35
|
handler.add_parse_error(e)
|
|
36
36
|
end
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
handler.result
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -13,11 +13,11 @@ module SvgConform
|
|
|
13
13
|
|
|
14
14
|
def initialize(profile)
|
|
15
15
|
@profile = profile
|
|
16
|
-
@element_stack = []
|
|
17
|
-
@path_stack = []
|
|
18
|
-
@position_counters = []
|
|
16
|
+
@element_stack = [] # Track parent-child hierarchy
|
|
17
|
+
@path_stack = [] # Current element path
|
|
18
|
+
@position_counters = [] # Stack of sibling counters per level
|
|
19
19
|
@parse_errors = []
|
|
20
|
-
@result = nil
|
|
20
|
+
@result = nil # Will be set in end_document
|
|
21
21
|
|
|
22
22
|
# Create validation context (without document reference for SAX)
|
|
23
23
|
@context = create_sax_context
|
|
@@ -50,13 +50,13 @@ module SvgConform
|
|
|
50
50
|
attributes: attrs,
|
|
51
51
|
position: position,
|
|
52
52
|
path: @path_stack.dup,
|
|
53
|
-
parent: @element_stack.last
|
|
53
|
+
parent: @element_stack.last,
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
# Push to stacks
|
|
57
57
|
@element_stack.push(element)
|
|
58
58
|
@path_stack.push("#{name}[#{position}]")
|
|
59
|
-
@position_counters.push({})
|
|
59
|
+
@position_counters.push({}) # New level for this element's children
|
|
60
60
|
|
|
61
61
|
# Validate with immediate requirements
|
|
62
62
|
@immediate_requirements.each do |req|
|
|
@@ -65,12 +65,15 @@ module SvgConform
|
|
|
65
65
|
|
|
66
66
|
# Deferred requirements may need to collect data
|
|
67
67
|
@deferred_requirements.each do |req|
|
|
68
|
-
|
|
68
|
+
if req.respond_to?(:collect_sax_data)
|
|
69
|
+
req.collect_sax_data(element,
|
|
70
|
+
@context)
|
|
71
|
+
end
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
# SAX Event: Element end tag
|
|
73
|
-
def end_element(
|
|
76
|
+
def end_element(_name)
|
|
74
77
|
@element_stack.pop
|
|
75
78
|
@path_stack.pop
|
|
76
79
|
@position_counters.pop
|
|
@@ -79,6 +82,7 @@ module SvgConform
|
|
|
79
82
|
# SAX Event: Text content
|
|
80
83
|
def characters(string)
|
|
81
84
|
return if @element_stack.empty?
|
|
85
|
+
|
|
82
86
|
@element_stack.last.text_content << string
|
|
83
87
|
end
|
|
84
88
|
|
|
@@ -109,7 +113,7 @@ module SvgConform
|
|
|
109
113
|
node: nil,
|
|
110
114
|
message: "Parse error: #{error.message}",
|
|
111
115
|
requirement_id: "parse_error",
|
|
112
|
-
severity: :error
|
|
116
|
+
severity: :error,
|
|
113
117
|
)
|
|
114
118
|
end
|
|
115
119
|
|
|
@@ -138,7 +142,7 @@ module SvgConform
|
|
|
138
142
|
context.instance_variable_set(:@data, {})
|
|
139
143
|
context.instance_variable_set(:@structurally_invalid_node_ids, Set.new)
|
|
140
144
|
context.instance_variable_set(:@node_id_cache, {})
|
|
141
|
-
context.instance_variable_set(:@cache_populated, true)
|
|
145
|
+
context.instance_variable_set(:@cache_populated, true) # Skip population for SAX
|
|
142
146
|
context
|
|
143
147
|
end
|
|
144
148
|
|