svg_conform 0.1.7 → 0.1.8
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 +79 -13
- data/docs/sax_validation_mode.adoc +576 -0
- data/lib/svg_conform/document.rb +4 -1
- data/lib/svg_conform/document_analyzer.rb +118 -0
- data/lib/svg_conform/errors/base.rb +58 -0
- data/lib/svg_conform/errors/validation_issue.rb +245 -0
- data/lib/svg_conform/errors/validation_notice.rb +30 -0
- data/lib/svg_conform/interfaces/requirement_interface.rb +177 -0
- data/lib/svg_conform/remediations/namespace_remediation.rb +2 -1
- data/lib/svg_conform/requirements/base_requirement.rb +16 -0
- data/lib/svg_conform/requirements/color_restrictions_requirement.rb +19 -61
- data/lib/svg_conform/requirements/font_family_requirement.rb +14 -24
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +19 -77
- data/lib/svg_conform/requirements/viewbox_required_requirement.rb +16 -72
- data/lib/svg_conform/sax_validation_handler.rb +15 -7
- data/lib/svg_conform/validation/error_tracker.rb +103 -0
- data/lib/svg_conform/validation/node_id_manager.rb +35 -0
- data/lib/svg_conform/validation/structural_invalidity_tracker.rb +63 -0
- data/lib/svg_conform/validation_context.rb +112 -459
- data/lib/svg_conform/validation_issue.rb +12 -0
- data/lib/svg_conform/version.rb +1 -1
- data/lib/svg_conform.rb +2 -0
- metadata +11 -2
- data/lib/svg_conform/fast_document_analyzer.rb +0 -82
|
@@ -27,49 +27,39 @@ module SvgConform
|
|
|
27
27
|
def check(node, context)
|
|
28
28
|
return unless element?(node)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
# Style properties are handled by StylePromotionRequirement to avoid duplication
|
|
32
|
-
font_family = get_attribute(node, "font-family")
|
|
33
|
-
return unless font_family
|
|
34
|
-
|
|
35
|
-
if svgcheck_compatibility
|
|
36
|
-
check_font_family_svgcheck_mode(node, context, font_family,
|
|
37
|
-
"font-family")
|
|
38
|
-
elsif !valid_font_family?(font_family)
|
|
39
|
-
context.add_error(
|
|
40
|
-
requirement_id: id,
|
|
41
|
-
message: "Font family '#{font_family}' is not allowed in this profile",
|
|
42
|
-
node: node,
|
|
43
|
-
severity: :error,
|
|
44
|
-
data: { attribute: "font-family", value: font_family },
|
|
45
|
-
)
|
|
46
|
-
end
|
|
30
|
+
validate_font_family(node, context)
|
|
47
31
|
end
|
|
48
32
|
|
|
49
33
|
def validate_sax_element(element, context)
|
|
50
|
-
# Skip if parent is structurally invalid
|
|
34
|
+
# Skip if parent is structurally invalid (SAX-specific: DOM handles this via should_check_node?)
|
|
51
35
|
return if element.parent && context.node_structurally_invalid?(element.parent)
|
|
52
36
|
|
|
53
|
-
|
|
54
|
-
|
|
37
|
+
validate_font_family(element, context)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Shared validation logic for both DOM and SAX modes
|
|
43
|
+
def validate_font_family(node_or_element, context)
|
|
44
|
+
# Check font-family attribute only (not style properties)
|
|
45
|
+
# Style properties are handled by StylePromotionRequirement to avoid duplication
|
|
46
|
+
font_family = get_attribute(node_or_element, "font-family")
|
|
55
47
|
return unless font_family
|
|
56
48
|
|
|
57
49
|
if svgcheck_compatibility
|
|
58
|
-
check_font_family_svgcheck_mode(
|
|
50
|
+
check_font_family_svgcheck_mode(node_or_element, context, font_family,
|
|
59
51
|
"font-family")
|
|
60
52
|
elsif !valid_font_family?(font_family)
|
|
61
53
|
context.add_error(
|
|
62
54
|
requirement_id: id,
|
|
63
55
|
message: "Font family '#{font_family}' is not allowed in this profile",
|
|
64
|
-
node:
|
|
56
|
+
node: node_or_element,
|
|
65
57
|
severity: :error,
|
|
66
58
|
data: { attribute: "font-family", value: font_family },
|
|
67
59
|
)
|
|
68
60
|
end
|
|
69
61
|
end
|
|
70
62
|
|
|
71
|
-
private
|
|
72
|
-
|
|
73
63
|
def check_font_family_svgcheck_mode(node, context, font_family_value,
|
|
74
64
|
attribute_name)
|
|
75
65
|
# Check if the font family value is valid according to svgcheck
|
|
@@ -57,7 +57,7 @@ module SvgConform
|
|
|
57
57
|
|
|
58
58
|
# Validate collected style elements
|
|
59
59
|
@collected_style_elements.each do |element|
|
|
60
|
-
|
|
60
|
+
check_style_element(element, context)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Reset for next validation
|
|
@@ -79,17 +79,22 @@ module SvgConform
|
|
|
79
79
|
# Style elements handled in deferred validation (need text content)
|
|
80
80
|
# Already collected in collect_sax_data
|
|
81
81
|
when "link"
|
|
82
|
-
|
|
82
|
+
check_link_element(element, context) if check_link_elements
|
|
83
83
|
else
|
|
84
|
-
|
|
84
|
+
check_style_attribute(element, context) if check_style_attributes
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
private
|
|
89
89
|
|
|
90
|
-
def check_style_element(
|
|
90
|
+
def check_style_element(node_or_element, context)
|
|
91
91
|
# Check for @import rules in style elements
|
|
92
|
-
|
|
92
|
+
# Handle both DOM nodes (node.text) and SAX elements (element.text_content)
|
|
93
|
+
content = if node_or_element.respond_to?(:text_content)
|
|
94
|
+
node_or_element.text_content
|
|
95
|
+
else
|
|
96
|
+
node_or_element.text || ""
|
|
97
|
+
end
|
|
93
98
|
|
|
94
99
|
if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
95
100
|
url = ::Regexp.last_match(1)
|
|
@@ -97,7 +102,7 @@ module SvgConform
|
|
|
97
102
|
context.add_error(
|
|
98
103
|
requirement_id: id,
|
|
99
104
|
message: "External CSS import not allowed: #{url}",
|
|
100
|
-
node:
|
|
105
|
+
node: node_or_element,
|
|
101
106
|
severity: :error,
|
|
102
107
|
)
|
|
103
108
|
end
|
|
@@ -111,14 +116,14 @@ module SvgConform
|
|
|
111
116
|
context.add_error(
|
|
112
117
|
requirement_id: id,
|
|
113
118
|
message: "External CSS import not allowed: #{url}",
|
|
114
|
-
node:
|
|
119
|
+
node: node_or_element,
|
|
115
120
|
severity: :error,
|
|
116
121
|
)
|
|
117
122
|
end
|
|
118
123
|
|
|
119
|
-
def check_link_element(
|
|
120
|
-
rel = get_attribute(
|
|
121
|
-
href = get_attribute(
|
|
124
|
+
def check_link_element(node_or_element, context)
|
|
125
|
+
rel = get_attribute(node_or_element, "rel")
|
|
126
|
+
href = get_attribute(node_or_element, "href")
|
|
122
127
|
|
|
123
128
|
return unless rel&.downcase == "stylesheet" && href
|
|
124
129
|
|
|
@@ -127,13 +132,13 @@ module SvgConform
|
|
|
127
132
|
context.add_error(
|
|
128
133
|
requirement_id: id,
|
|
129
134
|
message: "External CSS link not allowed: #{href}",
|
|
130
|
-
node:
|
|
135
|
+
node: node_or_element,
|
|
131
136
|
severity: :error,
|
|
132
137
|
)
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
def check_style_attribute(
|
|
136
|
-
style_value = get_attribute(
|
|
140
|
+
def check_style_attribute(node_or_element, context)
|
|
141
|
+
style_value = get_attribute(node_or_element, "style")
|
|
137
142
|
return unless style_value
|
|
138
143
|
|
|
139
144
|
# Check for url() references in style attributes
|
|
@@ -145,70 +150,7 @@ module SvgConform
|
|
|
145
150
|
context.add_error(
|
|
146
151
|
requirement_id: id,
|
|
147
152
|
message: "External URL reference in style attribute not allowed: #{url}",
|
|
148
|
-
node:
|
|
149
|
-
severity: :error,
|
|
150
|
-
)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def check_style_element_sax(element, context)
|
|
154
|
-
# Check for @import rules in style elements
|
|
155
|
-
content = element.text_content
|
|
156
|
-
|
|
157
|
-
if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
158
|
-
url = ::Regexp.last_match(1)
|
|
159
|
-
unless allowed_url?(url)
|
|
160
|
-
context.add_error(
|
|
161
|
-
requirement_id: id,
|
|
162
|
-
message: "External CSS import not allowed: #{url}",
|
|
163
|
-
node: element,
|
|
164
|
-
severity: :error,
|
|
165
|
-
)
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
return unless content =~ /@import\s+['"]([^'"]+)['"]/i
|
|
170
|
-
|
|
171
|
-
url = ::Regexp.last_match(1)
|
|
172
|
-
return if allowed_url?(url)
|
|
173
|
-
|
|
174
|
-
context.add_error(
|
|
175
|
-
requirement_id: id,
|
|
176
|
-
message: "External CSS import not allowed: #{url}",
|
|
177
|
-
node: element,
|
|
178
|
-
severity: :error,
|
|
179
|
-
)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def check_link_element_sax(element, context)
|
|
183
|
-
rel = element.raw_attributes["rel"]
|
|
184
|
-
href = element.raw_attributes["href"]
|
|
185
|
-
|
|
186
|
-
return unless rel&.downcase == "stylesheet" && href
|
|
187
|
-
|
|
188
|
-
return if allowed_url?(href)
|
|
189
|
-
|
|
190
|
-
context.add_error(
|
|
191
|
-
requirement_id: id,
|
|
192
|
-
message: "External CSS link not allowed: #{href}",
|
|
193
|
-
node: element,
|
|
194
|
-
severity: :error,
|
|
195
|
-
)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def check_style_attribute_sax(element, context)
|
|
199
|
-
style_value = element.raw_attributes["style"]
|
|
200
|
-
return unless style_value
|
|
201
|
-
|
|
202
|
-
# Check for url() references in style attributes
|
|
203
|
-
return unless style_value =~ /url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
|
|
204
|
-
|
|
205
|
-
url = ::Regexp.last_match(1)
|
|
206
|
-
return if allowed_url?(url)
|
|
207
|
-
|
|
208
|
-
context.add_error(
|
|
209
|
-
requirement_id: id,
|
|
210
|
-
message: "External URL reference in style attribute not allowed: #{url}",
|
|
211
|
-
node: element,
|
|
153
|
+
node: node_or_element,
|
|
212
154
|
severity: :error,
|
|
213
155
|
)
|
|
214
156
|
end
|
|
@@ -18,96 +18,41 @@ module SvgConform
|
|
|
18
18
|
root = document.root
|
|
19
19
|
return unless root&.name == "svg"
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if viewbox.nil? || viewbox.empty?
|
|
24
|
-
context.add_error(
|
|
25
|
-
requirement: self,
|
|
26
|
-
node: root,
|
|
27
|
-
message: "SVG root element must have a viewBox attribute",
|
|
28
|
-
data: { missing_attribute: "viewBox" },
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
# Add informational message about calculated viewBox if width/height are present
|
|
32
|
-
width = get_attribute(root, "width")
|
|
33
|
-
height = get_attribute(root, "height")
|
|
34
|
-
|
|
35
|
-
if width && height && valid_number?(width) && valid_number?(height)
|
|
36
|
-
calculated_viewbox = "0 0 #{width.to_f} #{height.to_f}"
|
|
37
|
-
context.add_error(
|
|
38
|
-
requirement: self,
|
|
39
|
-
node: root,
|
|
40
|
-
message: "Trying to put in the attribute with value '#{calculated_viewbox}'",
|
|
41
|
-
data: {
|
|
42
|
-
calculated_viewbox: calculated_viewbox,
|
|
43
|
-
source_width: width,
|
|
44
|
-
source_height: height,
|
|
45
|
-
},
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
return
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Validate viewBox format (should be "min-x min-y width height")
|
|
52
|
-
# Also accept comma-separated with parentheses like "(0, 0, 100, 100)" (svgcheck is lenient)
|
|
53
|
-
normalized_viewbox = viewbox.strip.gsub(/[(),]/, " ").squeeze(" ")
|
|
54
|
-
parts = normalized_viewbox.strip.split(/\s+/)
|
|
55
|
-
unless parts.length == 4 && parts.all? { |part| valid_number?(part) }
|
|
56
|
-
context.add_error(
|
|
57
|
-
requirement: self,
|
|
58
|
-
node: root,
|
|
59
|
-
message: "viewBox attribute must contain four numeric values (min-x min-y width height)",
|
|
60
|
-
data: {
|
|
61
|
-
viewbox_value: viewbox,
|
|
62
|
-
parsed_parts: parts,
|
|
63
|
-
},
|
|
64
|
-
)
|
|
65
|
-
return
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Check that width and height are positive
|
|
69
|
-
width = parts[2].to_f
|
|
70
|
-
height = parts[3].to_f
|
|
71
|
-
|
|
72
|
-
return unless width <= 0 || height <= 0
|
|
73
|
-
|
|
74
|
-
context.add_error(
|
|
75
|
-
requirement: self,
|
|
76
|
-
node: root,
|
|
77
|
-
message: "viewBox width and height must be positive values",
|
|
78
|
-
data: {
|
|
79
|
-
viewbox_value: viewbox,
|
|
80
|
-
width: width,
|
|
81
|
-
height: height,
|
|
82
|
-
},
|
|
83
|
-
)
|
|
21
|
+
validate_viewbox(root, context)
|
|
84
22
|
end
|
|
85
23
|
|
|
86
24
|
def validate_sax_element(element, context)
|
|
87
25
|
# Only check the root SVG element
|
|
88
26
|
return unless element.name == "svg" && element.parent.nil?
|
|
89
27
|
|
|
90
|
-
|
|
28
|
+
validate_viewbox(element, context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Shared validation logic for both DOM and SAX modes
|
|
34
|
+
def validate_viewbox(svg_root, context)
|
|
35
|
+
viewbox = get_attribute(svg_root, "viewBox")
|
|
91
36
|
|
|
92
37
|
if viewbox.nil? || viewbox.empty?
|
|
93
38
|
context.add_error(
|
|
94
39
|
requirement_id: id,
|
|
95
40
|
message: "SVG root element must have a viewBox attribute",
|
|
96
|
-
node:
|
|
41
|
+
node: svg_root,
|
|
97
42
|
severity: :error,
|
|
98
43
|
data: { missing_attribute: "viewBox" },
|
|
99
44
|
)
|
|
100
45
|
|
|
101
46
|
# Add informational message about calculated viewBox if width/height are present
|
|
102
|
-
width =
|
|
103
|
-
height =
|
|
47
|
+
width = get_attribute(svg_root, "width")
|
|
48
|
+
height = get_attribute(svg_root, "height")
|
|
104
49
|
|
|
105
50
|
if width && height && valid_number?(width) && valid_number?(height)
|
|
106
51
|
calculated_viewbox = "0 0 #{width.to_f} #{height.to_f}"
|
|
107
52
|
context.add_error(
|
|
108
53
|
requirement_id: id,
|
|
109
54
|
message: "Trying to put in the attribute with value '#{calculated_viewbox}'",
|
|
110
|
-
node:
|
|
55
|
+
node: svg_root,
|
|
111
56
|
severity: :error,
|
|
112
57
|
data: {
|
|
113
58
|
calculated_viewbox: calculated_viewbox,
|
|
@@ -120,13 +65,14 @@ module SvgConform
|
|
|
120
65
|
end
|
|
121
66
|
|
|
122
67
|
# Validate viewBox format (should be "min-x min-y width height")
|
|
68
|
+
# Also accept comma-separated with parentheses like "(0, 0, 100, 100)" (svgcheck is lenient)
|
|
123
69
|
normalized_viewbox = viewbox.strip.gsub(/[(),]/, " ").squeeze(" ")
|
|
124
70
|
parts = normalized_viewbox.strip.split(/\s+/)
|
|
125
71
|
unless parts.length == 4 && parts.all? { |part| valid_number?(part) }
|
|
126
72
|
context.add_error(
|
|
127
73
|
requirement_id: id,
|
|
128
74
|
message: "viewBox attribute must contain four numeric values (min-x min-y width height)",
|
|
129
|
-
node:
|
|
75
|
+
node: svg_root,
|
|
130
76
|
severity: :error,
|
|
131
77
|
data: {
|
|
132
78
|
viewbox_value: viewbox,
|
|
@@ -145,7 +91,7 @@ module SvgConform
|
|
|
145
91
|
context.add_error(
|
|
146
92
|
requirement_id: id,
|
|
147
93
|
message: "viewBox width and height must be positive values",
|
|
148
|
-
node:
|
|
94
|
+
node: svg_root,
|
|
149
95
|
severity: :error,
|
|
150
96
|
data: {
|
|
151
97
|
viewbox_value: viewbox,
|
|
@@ -155,8 +101,6 @@ module SvgConform
|
|
|
155
101
|
)
|
|
156
102
|
end
|
|
157
103
|
|
|
158
|
-
private
|
|
159
|
-
|
|
160
104
|
def valid_number?(str)
|
|
161
105
|
Float(str)
|
|
162
106
|
true
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "nokogiri"
|
|
4
4
|
require_relative "element_proxy"
|
|
5
5
|
require_relative "validation_context"
|
|
6
|
+
require_relative "validation/error_tracker"
|
|
7
|
+
require_relative "validation/structural_invalidity_tracker"
|
|
8
|
+
require_relative "validation/node_id_manager"
|
|
6
9
|
require_relative "validation_result"
|
|
7
10
|
require_relative "references"
|
|
8
11
|
|
|
@@ -139,14 +142,19 @@ module SvgConform
|
|
|
139
142
|
context = ValidationContext.allocate
|
|
140
143
|
context.instance_variable_set(:@document, nil)
|
|
141
144
|
context.instance_variable_set(:@profile, @profile)
|
|
142
|
-
context.instance_variable_set(:@
|
|
143
|
-
context.instance_variable_set(:@
|
|
144
|
-
context.instance_variable_set(:@validity_errors, [])
|
|
145
|
-
context.instance_variable_set(:@infos, [])
|
|
145
|
+
context.instance_variable_set(:@error_tracker, Validation::ErrorTracker.new)
|
|
146
|
+
context.instance_variable_set(:@fixes, [])
|
|
146
147
|
context.instance_variable_set(:@data, {})
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
context.instance_variable_set(:@
|
|
148
|
+
# Create NodeIdManager without a document (SAX mode)
|
|
149
|
+
node_id_manager = Validation::NodeIdManager.new(nil)
|
|
150
|
+
context.instance_variable_set(:@node_id_manager, node_id_manager)
|
|
151
|
+
# Create StructuralInvalidityTracker with a node ID generator
|
|
152
|
+
context.instance_variable_set(:@structural_invalidity_tracker,
|
|
153
|
+
Validation::StructuralInvalidityTracker.new(
|
|
154
|
+
node_id_generator: ->(node) {
|
|
155
|
+
node_id_manager.generate_node_id(node)
|
|
156
|
+
},
|
|
157
|
+
))
|
|
150
158
|
context.instance_variable_set(:@reference_manifest,
|
|
151
159
|
References::ReferenceManifest.new(source_document: nil))
|
|
152
160
|
context
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SvgConform
|
|
4
|
+
module Validation
|
|
5
|
+
# Tracks validation errors, warnings, and informational messages
|
|
6
|
+
# Separated from ValidationContext for better separation of concerns
|
|
7
|
+
#
|
|
8
|
+
# Note: This class references ValidationContext::ValidationIssue which is
|
|
9
|
+
# defined in ValidationContext. ValidationContext must be loaded first.
|
|
10
|
+
class ErrorTracker
|
|
11
|
+
attr_reader :errors, :warnings, :validity_errors, :infos
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@errors = []
|
|
15
|
+
@warnings = []
|
|
16
|
+
@validity_errors = []
|
|
17
|
+
@infos = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Add an error to the context
|
|
21
|
+
def add_error(node:, message:, rule: nil, requirement: nil,
|
|
22
|
+
requirement_id: nil, severity: nil, fix: nil, data: {})
|
|
23
|
+
# Support both old rule system and new requirements system
|
|
24
|
+
rule_or_requirement = requirement || rule
|
|
25
|
+
|
|
26
|
+
error = ::SvgConform::Errors::ValidationIssue.new(
|
|
27
|
+
type: :error,
|
|
28
|
+
rule: rule_or_requirement,
|
|
29
|
+
node: node,
|
|
30
|
+
message: message,
|
|
31
|
+
fix: fix,
|
|
32
|
+
data: data,
|
|
33
|
+
requirement_id: requirement_id,
|
|
34
|
+
severity: severity,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Handle special severity types
|
|
38
|
+
if severity == :validity_error
|
|
39
|
+
@validity_errors << error
|
|
40
|
+
else
|
|
41
|
+
@errors << error
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
error
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Add a warning to the context
|
|
48
|
+
def add_warning(rule:, node:, message:, fix: nil)
|
|
49
|
+
warning = ::SvgConform::Errors::ValidationIssue.new(
|
|
50
|
+
type: :warning,
|
|
51
|
+
rule: rule,
|
|
52
|
+
node: node,
|
|
53
|
+
message: message,
|
|
54
|
+
fix: fix,
|
|
55
|
+
)
|
|
56
|
+
@warnings << warning
|
|
57
|
+
warning
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Add an informational notice to the context
|
|
61
|
+
def add_notice(rule:, node:, message:, fix: nil, data: {}, type: :info)
|
|
62
|
+
notice = ::SvgConform::Errors::ValidationIssue.new(
|
|
63
|
+
type: type,
|
|
64
|
+
rule: rule,
|
|
65
|
+
node: node,
|
|
66
|
+
message: message,
|
|
67
|
+
fix: fix,
|
|
68
|
+
data: data,
|
|
69
|
+
)
|
|
70
|
+
@infos << notice
|
|
71
|
+
notice
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if there are any errors
|
|
75
|
+
def has_errors?
|
|
76
|
+
@errors.any?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if there are any warnings
|
|
80
|
+
def has_warnings?
|
|
81
|
+
@warnings.any?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if there are any validity errors
|
|
85
|
+
def has_validity_errors?
|
|
86
|
+
@validity_errors.any?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get total error count (including validity errors)
|
|
90
|
+
def total_error_count
|
|
91
|
+
@errors.length + @validity_errors.length
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clear all tracked issues
|
|
95
|
+
def clear
|
|
96
|
+
@errors.clear
|
|
97
|
+
@warnings.clear
|
|
98
|
+
@validity_errors.clear
|
|
99
|
+
@infos.clear
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../document_analyzer"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Validation
|
|
7
|
+
# Manages node ID generation for validation contexts
|
|
8
|
+
# Wraps DocumentAnalyzer for DOM mode and handles ElementProxy for SAX mode
|
|
9
|
+
class NodeIdManager
|
|
10
|
+
def initialize(document = nil)
|
|
11
|
+
@document = document
|
|
12
|
+
@analyzer = nil # Lazy-loaded, only needed for DOM/remediation mode
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate a unique identifier for a node based on its path
|
|
16
|
+
# Uses DocumentAnalyzer for efficient forward-counting algorithm (DOM mode)
|
|
17
|
+
# For SAX mode, uses the pre-computed path from ElementProxy
|
|
18
|
+
def generate_node_id(node)
|
|
19
|
+
# In SAX mode, node may be an ElementProxy with pre-computed path
|
|
20
|
+
return node.path_id if node.respond_to?(:path_id)
|
|
21
|
+
|
|
22
|
+
# Lazy-load the analyzer only when needed (DOM/remediation mode)
|
|
23
|
+
@analyzer ||= DocumentAnalyzer.new(@document) if @document
|
|
24
|
+
return nil unless @analyzer
|
|
25
|
+
|
|
26
|
+
@analyzer.get_node_id(node)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if the manager has a document for DOM-based ID generation
|
|
30
|
+
def dom_mode?
|
|
31
|
+
!@document.nil?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Validation
|
|
7
|
+
# Tracks structurally invalid nodes during validation
|
|
8
|
+
# Separated from ValidationContext for better separation of concerns
|
|
9
|
+
class StructuralInvalidityTracker
|
|
10
|
+
def initialize(node_id_generator:)
|
|
11
|
+
@structurally_invalid_node_ids = Set.new
|
|
12
|
+
@node_id_generator = node_id_generator
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Mark a node as structurally invalid (e.g., invalid parent-child relationship)
|
|
16
|
+
# Other requirements should skip attribute validation on these nodes
|
|
17
|
+
# Also marks all descendants as invalid since they'll be removed with the parent
|
|
18
|
+
def mark_node_structurally_invalid(node)
|
|
19
|
+
node_id = @node_id_generator.call(node)
|
|
20
|
+
return if node_id.nil? # Safety check
|
|
21
|
+
|
|
22
|
+
@structurally_invalid_node_ids.add(node_id)
|
|
23
|
+
|
|
24
|
+
# Mark all descendants as invalid too
|
|
25
|
+
mark_descendants_invalid(node)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Mark all descendants of a node as structurally invalid
|
|
29
|
+
def mark_descendants_invalid(node)
|
|
30
|
+
# In SAX mode, ElementProxy doesn't have children yet
|
|
31
|
+
# Children will be validated individually and will check parent validity
|
|
32
|
+
return unless node.respond_to?(:children) && node.children
|
|
33
|
+
|
|
34
|
+
node.children.each do |child|
|
|
35
|
+
child_id = @node_id_generator.call(child)
|
|
36
|
+
next if child_id.nil? # Skip if can't generate ID
|
|
37
|
+
|
|
38
|
+
@structurally_invalid_node_ids.add(child_id)
|
|
39
|
+
# Recursively mark descendants
|
|
40
|
+
mark_descendants_invalid(child)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if a node is structurally invalid
|
|
45
|
+
def node_structurally_invalid?(node)
|
|
46
|
+
node_id = @node_id_generator.call(node)
|
|
47
|
+
return false if node_id.nil? # Safety check
|
|
48
|
+
|
|
49
|
+
@structurally_invalid_node_ids.include?(node_id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Clear all tracked invalid nodes
|
|
53
|
+
def clear
|
|
54
|
+
@structurally_invalid_node_ids.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the count of structurally invalid nodes
|
|
58
|
+
def count
|
|
59
|
+
@structurally_invalid_node_ids.size
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|