svg_conform 0.1.6 → 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/Gemfile +2 -2
- data/docs/sax_validation_mode.adoc +576 -0
- data/lib/svg_conform/document.rb +25 -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/node_helpers.rb +72 -0
- data/lib/svg_conform/remediations/base_remediation.rb +5 -35
- data/lib/svg_conform/remediations/namespace_remediation.rb +61 -0
- data/lib/svg_conform/requirements/base_requirement.rb +21 -41
- 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 +13 -3
- data/lib/svg_conform/fast_document_analyzer.rb +0 -82
|
@@ -1,26 +1,105 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
|
+
require_relative "document_analyzer"
|
|
5
|
+
require_relative "errors/validation_issue"
|
|
6
|
+
require_relative "errors/validation_notice"
|
|
7
|
+
require_relative "validation/error_tracker"
|
|
8
|
+
require_relative "validation/structural_invalidity_tracker"
|
|
9
|
+
require_relative "validation/node_id_manager"
|
|
4
10
|
|
|
5
11
|
module SvgConform
|
|
6
|
-
# Context object passed to
|
|
12
|
+
# Context object passed to requirements during validation
|
|
13
|
+
#
|
|
14
|
+
# The ValidationContext serves as the central state management object
|
|
15
|
+
# during SVG validation. It tracks errors, warnings, structural issues,
|
|
16
|
+
# and provides helper methods for requirements to report issues.
|
|
17
|
+
#
|
|
18
|
+
# == Architecture
|
|
19
|
+
#
|
|
20
|
+
# ValidationContext delegates to specialized tracker classes:
|
|
21
|
+
# - ErrorTracker: tracks errors, warnings, and notices
|
|
22
|
+
# - StructuralInvalidityTracker: tracks structurally invalid nodes
|
|
23
|
+
# - NodeIdManager: handles node ID generation for both SAX and DOM modes
|
|
24
|
+
# - ReferenceManifest: tracks ID definitions and references
|
|
25
|
+
#
|
|
26
|
+
# == Usage
|
|
27
|
+
#
|
|
28
|
+
# In DOM validation (remediation mode):
|
|
29
|
+
# context = ValidationContext.new(document, profile)
|
|
30
|
+
# requirement.validate_document(document, context)
|
|
31
|
+
#
|
|
32
|
+
# In SAX validation (streaming mode):
|
|
33
|
+
# context = ValidationContext.new(nil, profile)
|
|
34
|
+
# requirement.validate_sax_element(element, context)
|
|
35
|
+
#
|
|
36
|
+
# == Error Reporting
|
|
37
|
+
#
|
|
38
|
+
# context.add_error(
|
|
39
|
+
# requirement_id: id,
|
|
40
|
+
# message: "Element not allowed",
|
|
41
|
+
# node: node,
|
|
42
|
+
# severity: :error,
|
|
43
|
+
# fix: -> { remove_element(node) }
|
|
44
|
+
# )
|
|
45
|
+
#
|
|
46
|
+
# == Node ID Tracking
|
|
47
|
+
#
|
|
48
|
+
# The context generates unique node IDs for tracking:
|
|
49
|
+
# - SAX mode: uses pre-computed path_id from ElementProxy
|
|
50
|
+
# - DOM mode: uses DocumentAnalyzer with forward-counting algorithm
|
|
51
|
+
#
|
|
52
|
+
# == Structural Invalidity
|
|
53
|
+
#
|
|
54
|
+
# Requirements can mark nodes as structurally invalid (e.g., invalid
|
|
55
|
+
# parent-child relationships). Other requirements should skip
|
|
56
|
+
# validation on these nodes since they will be removed during remediation.
|
|
57
|
+
#
|
|
58
|
+
# context.mark_node_structurally_invalid(node)
|
|
59
|
+
# context.node_structurally_invalid?(node) # => true
|
|
60
|
+
#
|
|
7
61
|
class ValidationContext
|
|
8
|
-
|
|
9
|
-
|
|
62
|
+
# The document being validated (nil in SAX mode)
|
|
63
|
+
attr_reader :document
|
|
64
|
+
|
|
65
|
+
# The profile containing requirements to validate against
|
|
66
|
+
attr_reader :profile
|
|
67
|
+
|
|
68
|
+
# Array of fixes that can be applied
|
|
69
|
+
attr_reader :fixes
|
|
70
|
+
|
|
71
|
+
# Reference manifest tracking IDs and references
|
|
72
|
+
attr_reader :reference_manifest
|
|
73
|
+
|
|
74
|
+
# Delegate error tracking methods to ErrorTracker
|
|
75
|
+
def errors
|
|
76
|
+
@error_tracker.errors
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def warnings
|
|
80
|
+
@error_tracker.warnings
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validity_errors
|
|
84
|
+
@error_tracker.validity_errors
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def infos
|
|
88
|
+
@error_tracker.infos
|
|
89
|
+
end
|
|
10
90
|
|
|
11
91
|
def initialize(document, profile)
|
|
12
92
|
@document = document
|
|
13
93
|
@profile = profile
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@validity_errors = []
|
|
17
|
-
@infos = []
|
|
94
|
+
@error_tracker = Validation::ErrorTracker.new
|
|
95
|
+
@fixes = []
|
|
18
96
|
@data = {}
|
|
19
|
-
@
|
|
20
|
-
@
|
|
21
|
-
|
|
97
|
+
@node_id_manager = Validation::NodeIdManager.new(document)
|
|
98
|
+
@structural_invalidity_tracker = Validation::StructuralInvalidityTracker.new(
|
|
99
|
+
node_id_generator: ->(node) { @node_id_manager.generate_node_id(node) },
|
|
100
|
+
)
|
|
22
101
|
@reference_manifest = References::ReferenceManifest.new(
|
|
23
|
-
source_document: document
|
|
102
|
+
source_document: document&.file_path,
|
|
24
103
|
)
|
|
25
104
|
end
|
|
26
105
|
|
|
@@ -28,75 +107,36 @@ module SvgConform
|
|
|
28
107
|
# Other requirements should skip attribute validation on these nodes
|
|
29
108
|
# Also marks all descendants as invalid since they'll be removed with the parent
|
|
30
109
|
def mark_node_structurally_invalid(node)
|
|
31
|
-
|
|
32
|
-
return if node_id.nil? # Safety check
|
|
33
|
-
|
|
34
|
-
@structurally_invalid_node_ids.add(node_id)
|
|
35
|
-
|
|
36
|
-
# Mark all descendants as invalid too
|
|
37
|
-
mark_descendants_invalid(node)
|
|
110
|
+
@structural_invalidity_tracker.mark_node_structurally_invalid(node)
|
|
38
111
|
end
|
|
39
112
|
|
|
40
113
|
# Mark all descendants of a node as structurally invalid
|
|
41
114
|
def mark_descendants_invalid(node)
|
|
42
|
-
|
|
43
|
-
# Children will be validated individually and will check parent validity
|
|
44
|
-
return unless node.respond_to?(:children) && node.children
|
|
45
|
-
|
|
46
|
-
node.children.each do |child|
|
|
47
|
-
child_id = generate_node_id(child)
|
|
48
|
-
next if child_id.nil? # Skip if can't generate ID
|
|
49
|
-
|
|
50
|
-
@structurally_invalid_node_ids.add(child_id)
|
|
51
|
-
# Recursively mark descendants
|
|
52
|
-
mark_descendants_invalid(child)
|
|
53
|
-
end
|
|
115
|
+
@structural_invalidity_tracker.mark_descendants_invalid(node)
|
|
54
116
|
end
|
|
55
117
|
|
|
56
118
|
# Check if a node is structurally invalid
|
|
57
119
|
def node_structurally_invalid?(node)
|
|
58
|
-
|
|
59
|
-
return false if node_id.nil? # Safety check
|
|
60
|
-
|
|
61
|
-
@structurally_invalid_node_ids.include?(node_id)
|
|
120
|
+
@structural_invalidity_tracker.node_structurally_invalid?(node)
|
|
62
121
|
end
|
|
63
122
|
|
|
64
123
|
def add_error(node:, message:, rule: nil, requirement: nil,
|
|
65
124
|
requirement_id: nil, severity: nil, fix: nil, data: {})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
error = ValidationIssue.new(
|
|
70
|
-
type: :error,
|
|
71
|
-
rule: rule_or_requirement,
|
|
72
|
-
node: node,
|
|
73
|
-
message: message,
|
|
74
|
-
fix: fix,
|
|
75
|
-
data: data,
|
|
76
|
-
requirement_id: requirement_id,
|
|
77
|
-
severity: severity,
|
|
125
|
+
@error_tracker.add_error(
|
|
126
|
+
node: node, message: message, rule: rule, requirement: requirement,
|
|
127
|
+
requirement_id: requirement_id, severity: severity, fix: fix, data: data
|
|
78
128
|
)
|
|
79
|
-
|
|
80
|
-
# Handle special severity types
|
|
81
|
-
if severity == :validity_error
|
|
82
|
-
@validity_errors << error
|
|
83
|
-
else
|
|
84
|
-
@errors << error
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
error
|
|
88
129
|
end
|
|
89
130
|
|
|
90
131
|
def add_warning(rule:, node:, message:, fix: nil)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
warning
|
|
132
|
+
@error_tracker.add_warning(rule: rule, node: node, message: message,
|
|
133
|
+
fix: fix)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Add an informational notice (delegates to ErrorTracker)
|
|
137
|
+
def add_notice(rule:, node:, message:, fix: nil, data: {})
|
|
138
|
+
@error_tracker.add_notice(rule: rule, node: node, message: message,
|
|
139
|
+
fix: fix, data: data)
|
|
100
140
|
end
|
|
101
141
|
|
|
102
142
|
def add_fix(fix)
|
|
@@ -104,11 +144,11 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
|
|
|
104
144
|
end
|
|
105
145
|
|
|
106
146
|
def has_errors?
|
|
107
|
-
|
|
147
|
+
@error_tracker.has_errors?
|
|
108
148
|
end
|
|
109
149
|
|
|
110
150
|
def has_warnings?
|
|
111
|
-
|
|
151
|
+
@error_tracker.has_warnings?
|
|
112
152
|
end
|
|
113
153
|
|
|
114
154
|
def has_fixes?
|
|
@@ -116,11 +156,11 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
|
|
|
116
156
|
end
|
|
117
157
|
|
|
118
158
|
def issue_count
|
|
119
|
-
@
|
|
159
|
+
@error_tracker.total_error_count + warnings.size
|
|
120
160
|
end
|
|
121
161
|
|
|
122
162
|
def fixable_count
|
|
123
|
-
(@errors + @warnings).count(&:fixable?)
|
|
163
|
+
(@error_tracker.errors + @error_tracker.warnings).count(&:fixable?)
|
|
124
164
|
end
|
|
125
165
|
|
|
126
166
|
def set_data(key, value)
|
|
@@ -154,405 +194,18 @@ column_number: nil)
|
|
|
154
194
|
|
|
155
195
|
# Add notice for external references (not errors)
|
|
156
196
|
def add_external_reference_notice(node:, reference:, message: nil)
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
@error_tracker.add_notice(
|
|
198
|
+
rule: nil,
|
|
159
199
|
node: node,
|
|
160
200
|
message: message || "External reference: #{reference.value}",
|
|
161
201
|
data: { reference: reference },
|
|
162
202
|
)
|
|
163
|
-
@infos << notice
|
|
164
|
-
notice
|
|
165
203
|
end
|
|
166
204
|
|
|
167
205
|
# Generate a unique identifier for a node based on its path
|
|
168
|
-
#
|
|
169
|
-
# OPTIMIZED: Lazy cache population - populate entire cache on first call
|
|
206
|
+
# Delegates to NodeIdManager which handles both SAX and DOM modes
|
|
170
207
|
def generate_node_id(node)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# Populate cache for ALL nodes on first access
|
|
174
|
-
unless @cache_populated
|
|
175
|
-
populate_node_id_cache
|
|
176
|
-
@cache_populated = true
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Try cache lookup first
|
|
180
|
-
cached_id = @node_id_cache[node]
|
|
181
|
-
return cached_id if cached_id
|
|
182
|
-
|
|
183
|
-
# Fall back to building path if node not in cache
|
|
184
|
-
# (happens when different traversals create different wrapper objects)
|
|
185
|
-
build_node_path(node)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
private
|
|
189
|
-
|
|
190
|
-
# Populate cache for all nodes using document.traverse with parent tracking
|
|
191
|
-
def populate_node_id_cache
|
|
192
|
-
parent_stack = []
|
|
193
|
-
counter_stack = [{}] # Stack of {element_name => count} hashes
|
|
194
|
-
|
|
195
|
-
@document.traverse do |node|
|
|
196
|
-
next unless node.respond_to?(:name) && node.name
|
|
197
|
-
|
|
198
|
-
# Detect parent changes by checking node.parent
|
|
199
|
-
current_parent = node.respond_to?(:parent) ? node.parent : nil
|
|
200
|
-
|
|
201
|
-
# Adjust stack based on actual parent
|
|
202
|
-
while parent_stack.size.positive? && !parent_stack.last.equal?(current_parent)
|
|
203
|
-
parent_stack.pop
|
|
204
|
-
counter_stack.pop
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# If we have a new parent level, push it
|
|
208
|
-
if current_parent && (parent_stack.empty? || !parent_stack.last.equal?(current_parent))
|
|
209
|
-
parent_stack.push(current_parent)
|
|
210
|
-
counter_stack.push({})
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Increment counter at current level
|
|
214
|
-
current_counters = counter_stack.last || {}
|
|
215
|
-
current_counters[node.name] ||= 0
|
|
216
|
-
current_counters[node.name] += 1
|
|
217
|
-
|
|
218
|
-
# Build path using original backward logic (for correctness)
|
|
219
|
-
@node_id_cache[node] = build_node_path(node)
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Traverse tree, building paths with forward position counters
|
|
224
|
-
def traverse_with_forward_counting(node, path_parts, sibling_counters)
|
|
225
|
-
return unless node.respond_to?(:name) && node.name
|
|
226
|
-
|
|
227
|
-
# Increment counter for this node name at current level
|
|
228
|
-
sibling_counters[node.name] ||= 0
|
|
229
|
-
sibling_counters[node.name] += 1
|
|
230
|
-
position = sibling_counters[node.name]
|
|
231
|
-
|
|
232
|
-
# Build and cache path
|
|
233
|
-
current_path = path_parts + ["#{node.name}[#{position}]"]
|
|
234
|
-
@node_id_cache[node] = "/#{current_path.join('/')}"
|
|
235
|
-
|
|
236
|
-
# Traverse children with fresh counters
|
|
237
|
-
if node.respond_to?(:children)
|
|
238
|
-
child_counters = {}
|
|
239
|
-
node.children.each do |child|
|
|
240
|
-
traverse_with_forward_counting(child, current_path, child_counters)
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Build path-based ID for a node (original logic, unchanged)
|
|
246
|
-
def build_node_path(node)
|
|
247
|
-
path_parts = []
|
|
248
|
-
current = node
|
|
249
|
-
|
|
250
|
-
while current
|
|
251
|
-
if current.respond_to?(:name) && current.name
|
|
252
|
-
# Count previous siblings of the same type for position (ORIGINAL LOGIC)
|
|
253
|
-
position = 1
|
|
254
|
-
if current.respond_to?(:previous_sibling)
|
|
255
|
-
sibling = current.previous_sibling
|
|
256
|
-
while sibling
|
|
257
|
-
position += 1 if sibling.respond_to?(:name) && sibling.name == current.name
|
|
258
|
-
sibling = sibling.previous_sibling if sibling.respond_to?(:previous_sibling)
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
path_parts.unshift("#{current.name}[#{position}]")
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Stop if we reach the document root (doesn't have parent)
|
|
266
|
-
break unless current.respond_to?(:parent)
|
|
267
|
-
|
|
268
|
-
begin
|
|
269
|
-
current = current.parent
|
|
270
|
-
rescue NoMethodError
|
|
271
|
-
# Parent method failed, we're at root
|
|
272
|
-
break
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
break unless current
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
"/#{path_parts.join('/')}"
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Base class for validation issues
|
|
283
|
-
class ValidationIssue
|
|
284
|
-
attr_reader :type, :rule, :node, :message, :fix, :data,
|
|
285
|
-
:requirement_id_override, :severity
|
|
286
|
-
|
|
287
|
-
def initialize(type:, rule:, node:, message:, fix: nil, data: {},
|
|
288
|
-
requirement_id: nil, severity: nil, violation_type: nil)
|
|
289
|
-
@type = type
|
|
290
|
-
@rule = rule
|
|
291
|
-
@node = node
|
|
292
|
-
@message = message
|
|
293
|
-
@fix = fix
|
|
294
|
-
@data = data
|
|
295
|
-
@requirement_id_override = requirement_id
|
|
296
|
-
@severity = severity
|
|
297
|
-
@violation_type = violation_type || detect_violation_type
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def requirement_id
|
|
301
|
-
return @requirement_id_override.to_s if @requirement_id_override
|
|
302
|
-
|
|
303
|
-
if @rule.respond_to?(:id)
|
|
304
|
-
@rule.id.to_s
|
|
305
|
-
elsif @rule.respond_to?(:class) && @rule.class.respond_to?(:name)
|
|
306
|
-
# Extract ID from class name for requirements
|
|
307
|
-
class_name = @rule.class.name.split("::").last
|
|
308
|
-
class_name.gsub(/Requirement$/, "").downcase.gsub(/([a-z])([A-Z])/,
|
|
309
|
-
'\1_\2').downcase
|
|
310
|
-
else
|
|
311
|
-
"unknown"
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def error?
|
|
316
|
-
@type == :error
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
def warning?
|
|
320
|
-
@type == :warning
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def fixable?
|
|
324
|
-
!@fix.nil?
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
def line
|
|
328
|
-
@node.respond_to?(:line) ? @node.line : nil
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def column
|
|
332
|
-
@node.respond_to?(:column) ? @node.column : nil
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def element_name
|
|
336
|
-
@node.respond_to?(:name) ? @node.name : nil
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def apply_fix
|
|
340
|
-
return false unless fixable?
|
|
341
|
-
|
|
342
|
-
begin
|
|
343
|
-
if @fix.respond_to?(:call)
|
|
344
|
-
@fix.call
|
|
345
|
-
else
|
|
346
|
-
@fix.apply
|
|
347
|
-
end
|
|
348
|
-
true
|
|
349
|
-
rescue StandardError
|
|
350
|
-
false
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def to_h
|
|
355
|
-
{
|
|
356
|
-
type: @type,
|
|
357
|
-
rule: rule_id,
|
|
358
|
-
message: @message,
|
|
359
|
-
line: line,
|
|
360
|
-
column: column,
|
|
361
|
-
element: element_name,
|
|
362
|
-
fixable: fixable?,
|
|
363
|
-
}
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def violation_type
|
|
367
|
-
@violation_type
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def remediable?
|
|
371
|
-
case @violation_type
|
|
372
|
-
when :color_violation, :font_violation, :content_violation, :reference_violation,
|
|
373
|
-
:namespace_violation, :viewbox_violation, :style_violation
|
|
374
|
-
true
|
|
375
|
-
when :structural_violation
|
|
376
|
-
false
|
|
377
|
-
else
|
|
378
|
-
# Default to remediable for backward compatibility
|
|
379
|
-
true
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
def remediation_type
|
|
384
|
-
case @violation_type
|
|
385
|
-
when :color_violation, :font_violation
|
|
386
|
-
:convert
|
|
387
|
-
when :content_violation, :reference_violation, :namespace_violation
|
|
388
|
-
:remove
|
|
389
|
-
when :viewbox_violation
|
|
390
|
-
:add
|
|
391
|
-
when :style_violation
|
|
392
|
-
:promote
|
|
393
|
-
else
|
|
394
|
-
:unknown
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def remediation_confidence
|
|
399
|
-
case @violation_type
|
|
400
|
-
when :color_violation, :font_violation, :style_violation
|
|
401
|
-
:automatic
|
|
402
|
-
when :viewbox_violation, :namespace_violation
|
|
403
|
-
:safe_structural
|
|
404
|
-
when :content_violation, :reference_violation
|
|
405
|
-
:safe_removal
|
|
406
|
-
else
|
|
407
|
-
:manual_review
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def suggested_action
|
|
412
|
-
case @violation_type
|
|
413
|
-
when :color_violation
|
|
414
|
-
"Convert color to allowed equivalent using CssColor"
|
|
415
|
-
when :font_violation
|
|
416
|
-
"Map font family to generic equivalent"
|
|
417
|
-
when :content_violation
|
|
418
|
-
"Remove forbidden element or attribute"
|
|
419
|
-
when :reference_violation
|
|
420
|
-
"Remove broken reference or containing element"
|
|
421
|
-
when :namespace_violation
|
|
422
|
-
"Fix namespace declarations and remove invalid elements/attributes"
|
|
423
|
-
when :viewbox_violation
|
|
424
|
-
"Add missing viewBox attribute"
|
|
425
|
-
when :style_violation
|
|
426
|
-
"Promote style properties to attributes"
|
|
427
|
-
when :structural_violation
|
|
428
|
-
"Manual fix required - document structure issue"
|
|
429
|
-
else
|
|
430
|
-
"Apply available remediation"
|
|
431
|
-
end
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
def affects_content?
|
|
435
|
-
case @violation_type
|
|
436
|
-
when :content_violation, :reference_violation
|
|
437
|
-
true
|
|
438
|
-
when :color_violation, :font_violation, :style_violation, :viewbox_violation, :namespace_violation
|
|
439
|
-
false
|
|
440
|
-
else
|
|
441
|
-
false
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def to_s
|
|
446
|
-
location = line ? " at line #{line}" : ""
|
|
447
|
-
location += ":#{column}" if column
|
|
448
|
-
rule_info = rule_id ? " (#{rule_id})" : ""
|
|
449
|
-
remediation_info = remediable? ? " [#{remediation_type}]" : " [NOT REMEDIABLE]"
|
|
450
|
-
"#{@message}#{location}#{rule_info}#{remediation_info}"
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
def to_h
|
|
454
|
-
{
|
|
455
|
-
type: @type,
|
|
456
|
-
rule: rule_id,
|
|
457
|
-
message: @message,
|
|
458
|
-
line: line,
|
|
459
|
-
column: column,
|
|
460
|
-
element: element_name,
|
|
461
|
-
fixable: fixable?,
|
|
462
|
-
remediable: remediable?,
|
|
463
|
-
violation_type: @violation_type,
|
|
464
|
-
remediation_type: remediation_type,
|
|
465
|
-
remediation_confidence: remediation_confidence,
|
|
466
|
-
suggested_action: suggested_action,
|
|
467
|
-
affects_content: affects_content?,
|
|
468
|
-
}
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
private
|
|
472
|
-
|
|
473
|
-
def detect_violation_type
|
|
474
|
-
req_id = requirement_id.downcase
|
|
475
|
-
msg = @message.downcase
|
|
476
|
-
|
|
477
|
-
# First check for structural violations (non-remediable)
|
|
478
|
-
return :structural_violation if msg.include?("root element must be") ||
|
|
479
|
-
msg.include?("malformed") ||
|
|
480
|
-
msg.include?("invalid document") ||
|
|
481
|
-
msg.include?("required element missing") ||
|
|
482
|
-
msg.include?("invalid hierarchy")
|
|
483
|
-
|
|
484
|
-
# Then check requirement-based violations (remediable)
|
|
485
|
-
case req_id
|
|
486
|
-
when /color/
|
|
487
|
-
:color_violation
|
|
488
|
-
when /font/
|
|
489
|
-
:font_violation
|
|
490
|
-
when /forbidden|content/
|
|
491
|
-
:content_violation
|
|
492
|
-
when /reference|id/
|
|
493
|
-
:reference_violation
|
|
494
|
-
when /namespace/
|
|
495
|
-
:namespace_violation
|
|
496
|
-
when /viewbox/
|
|
497
|
-
:viewbox_violation
|
|
498
|
-
when /style/
|
|
499
|
-
:style_violation
|
|
500
|
-
else
|
|
501
|
-
# Check message content for clues
|
|
502
|
-
return :color_violation if msg.include?("color")
|
|
503
|
-
return :font_violation if msg.include?("font")
|
|
504
|
-
return :content_violation if msg.include?("forbidden")
|
|
505
|
-
return :reference_violation if msg.include?("reference") || msg.include?("href")
|
|
506
|
-
return :namespace_violation if msg.include?("namespace")
|
|
507
|
-
return :viewbox_violation if msg.include?("viewbox")
|
|
508
|
-
return :style_violation if msg.include?("style")
|
|
509
|
-
|
|
510
|
-
:unknown_violation
|
|
511
|
-
end
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def rule_id
|
|
515
|
-
return @requirement_id_override if @requirement_id_override
|
|
516
|
-
return @rule.id if @rule.respond_to?(:id)
|
|
517
|
-
|
|
518
|
-
if @rule.respond_to?(:class) && @rule.class.respond_to?(:name)
|
|
519
|
-
# Extract ID from class name for requirements
|
|
520
|
-
class_name = @rule.class.name.split("::").last
|
|
521
|
-
class_name.gsub(/Requirement$/, "").downcase.gsub(/([a-z])([A-Z])/,
|
|
522
|
-
'\1_\2').downcase
|
|
523
|
-
else
|
|
524
|
-
"unknown"
|
|
525
|
-
end
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
# New class for non-error notifications
|
|
530
|
-
class ValidationNotice
|
|
531
|
-
attr_reader :type, :node, :message, :data
|
|
532
|
-
|
|
533
|
-
def initialize(type:, node:, message:, data: {})
|
|
534
|
-
@type = type
|
|
535
|
-
@node = node
|
|
536
|
-
@message = message
|
|
537
|
-
@data = data
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def line
|
|
541
|
-
@node.respond_to?(:line) ? @node.line : nil
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
def column
|
|
545
|
-
@node.respond_to?(:column) ? @node.column : nil
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
def to_h
|
|
549
|
-
{
|
|
550
|
-
type: @type,
|
|
551
|
-
message: @message,
|
|
552
|
-
line: line,
|
|
553
|
-
column: column,
|
|
554
|
-
data: @data,
|
|
555
|
-
}
|
|
208
|
+
@node_id_manager.generate_node_id(node)
|
|
556
209
|
end
|
|
557
210
|
end
|
|
558
211
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors/validation_issue"
|
|
4
|
+
require_relative "errors/validation_notice"
|
|
5
|
+
|
|
6
|
+
# Backward compatibility: re-export error classes at top level
|
|
7
|
+
module SvgConform
|
|
8
|
+
# Aliases for backward compatibility
|
|
9
|
+
# These classes are now defined in SvgConform::Errors
|
|
10
|
+
ValidationIssue = Errors::ValidationIssue
|
|
11
|
+
ValidationNotice = Errors::ValidationNotice
|
|
12
|
+
end
|
data/lib/svg_conform/version.rb
CHANGED
data/lib/svg_conform.rb
CHANGED
|
@@ -16,6 +16,8 @@ require_relative "svg_conform/profiles"
|
|
|
16
16
|
require_relative "svg_conform/document"
|
|
17
17
|
require_relative "svg_conform/sax_document"
|
|
18
18
|
require_relative "svg_conform/element_proxy"
|
|
19
|
+
require_relative "svg_conform/errors/validation_issue"
|
|
20
|
+
require_relative "svg_conform/errors/validation_notice"
|
|
19
21
|
require_relative "svg_conform/validation_context"
|
|
20
22
|
require_relative "svg_conform/validation_result"
|
|
21
23
|
require_relative "svg_conform/validator"
|