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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SvgConform
|
|
4
|
+
module Errors
|
|
5
|
+
# Base class for all SvgConform error types
|
|
6
|
+
#
|
|
7
|
+
# Provides common interface for error and notice objects
|
|
8
|
+
# used during validation.
|
|
9
|
+
class Base
|
|
10
|
+
attr_reader :type, :node, :message
|
|
11
|
+
|
|
12
|
+
# Initialize a new error/notice
|
|
13
|
+
#
|
|
14
|
+
# @param type [Symbol] The type (:error, :warning, :info, etc.)
|
|
15
|
+
# @param node [Object] The node associated with this error
|
|
16
|
+
# @param message [String] The error message
|
|
17
|
+
def initialize(type:, node:, message:)
|
|
18
|
+
@type = type
|
|
19
|
+
@node = node
|
|
20
|
+
@message = message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the line number of the error
|
|
24
|
+
#
|
|
25
|
+
# @return [Integer, nil] The line number if available
|
|
26
|
+
def line
|
|
27
|
+
@node.respond_to?(:line) ? @node.line : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the column number of the error
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer, nil] The column number if available
|
|
33
|
+
def column
|
|
34
|
+
@node.respond_to?(:column) ? @node.column : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get the element name of the error
|
|
38
|
+
#
|
|
39
|
+
# @return [String, nil] The element name if available
|
|
40
|
+
def element_name
|
|
41
|
+
@node.respond_to?(:name) ? @node.name : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Convert error to hash representation
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash] Hash representation of the error
|
|
47
|
+
def to_h
|
|
48
|
+
{
|
|
49
|
+
type: @type,
|
|
50
|
+
message: @message,
|
|
51
|
+
line: line,
|
|
52
|
+
column: column,
|
|
53
|
+
element: element_name,
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Errors
|
|
7
|
+
# Validation issue (error, warning, or notice)
|
|
8
|
+
#
|
|
9
|
+
# Represents a single validation issue found during SVG validation.
|
|
10
|
+
# Contains information about the issue including type, rule, node,
|
|
11
|
+
# message, fix, and remediation information.
|
|
12
|
+
class ValidationIssue < Base
|
|
13
|
+
attr_reader :rule, :fix, :data,
|
|
14
|
+
:requirement_id_override, :severity, :violation_type
|
|
15
|
+
|
|
16
|
+
def initialize(type:, rule:, node:, message:, fix: nil, data: {},
|
|
17
|
+
requirement_id: nil, severity: nil, violation_type: nil)
|
|
18
|
+
super(type: type, node: node, message: message)
|
|
19
|
+
@rule = rule
|
|
20
|
+
@fix = fix
|
|
21
|
+
@data = data
|
|
22
|
+
@requirement_id_override = requirement_id
|
|
23
|
+
@severity = severity
|
|
24
|
+
@violation_type = violation_type || detect_violation_type
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def requirement_id
|
|
28
|
+
return @requirement_id_override.to_s if @requirement_id_override
|
|
29
|
+
|
|
30
|
+
if @rule.respond_to?(:id)
|
|
31
|
+
@rule.id.to_s
|
|
32
|
+
elsif @rule.respond_to?(:class) && @rule.class.respond_to?(:name)
|
|
33
|
+
# Extract ID from class name for requirements
|
|
34
|
+
class_name = @rule.class.name.split("::").last
|
|
35
|
+
class_name.gsub(/Requirement$/, "").downcase.gsub(/([a-z])([A-Z])/,
|
|
36
|
+
'\1_\2').downcase
|
|
37
|
+
else
|
|
38
|
+
"unknown"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def error?
|
|
43
|
+
@type == :error
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def warning?
|
|
47
|
+
@type == :warning
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fixable?
|
|
51
|
+
!@fix.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def line
|
|
55
|
+
@node.respond_to?(:line) ? @node.line : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def column
|
|
59
|
+
@node.respond_to?(:column) ? @node.column : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def element_name
|
|
63
|
+
@node.respond_to?(:name) ? @node.name : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_fix
|
|
67
|
+
return false unless fixable?
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
if @fix.respond_to?(:call)
|
|
71
|
+
@fix.call
|
|
72
|
+
else
|
|
73
|
+
@fix.apply
|
|
74
|
+
end
|
|
75
|
+
true
|
|
76
|
+
rescue StandardError
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if this issue is remediable
|
|
82
|
+
def remediable?
|
|
83
|
+
case @violation_type
|
|
84
|
+
when :color_violation, :font_violation, :content_violation, :reference_violation,
|
|
85
|
+
:namespace_violation, :viewbox_violation, :style_violation
|
|
86
|
+
true
|
|
87
|
+
when :structural_violation
|
|
88
|
+
false
|
|
89
|
+
else
|
|
90
|
+
# Default to remediable for backward compatibility
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get the type of remediation needed
|
|
96
|
+
def remediation_type
|
|
97
|
+
case @violation_type
|
|
98
|
+
when :color_violation, :font_violation
|
|
99
|
+
:convert
|
|
100
|
+
when :content_violation, :reference_violation, :namespace_violation
|
|
101
|
+
:remove
|
|
102
|
+
when :viewbox_violation
|
|
103
|
+
:add
|
|
104
|
+
when :style_violation
|
|
105
|
+
:promote
|
|
106
|
+
else
|
|
107
|
+
:unknown
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get the confidence level of automated remediation
|
|
112
|
+
def remediation_confidence
|
|
113
|
+
case @violation_type
|
|
114
|
+
when :color_violation, :font_violation, :style_violation
|
|
115
|
+
:automatic
|
|
116
|
+
when :viewbox_violation, :namespace_violation
|
|
117
|
+
:safe_structural
|
|
118
|
+
when :content_violation, :reference_violation
|
|
119
|
+
:safe_removal
|
|
120
|
+
else
|
|
121
|
+
:manual_review
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get the suggested action to fix this issue
|
|
126
|
+
def suggested_action
|
|
127
|
+
case @violation_type
|
|
128
|
+
when :color_violation
|
|
129
|
+
"Convert color to allowed equivalent using CssColor"
|
|
130
|
+
when :font_violation
|
|
131
|
+
"Map font family to generic equivalent"
|
|
132
|
+
when :content_violation
|
|
133
|
+
"Remove forbidden element or attribute"
|
|
134
|
+
when :reference_violation
|
|
135
|
+
"Remove broken reference or containing element"
|
|
136
|
+
when :namespace_violation
|
|
137
|
+
"Fix namespace declarations and remove invalid elements/attributes"
|
|
138
|
+
when :viewbox_violation
|
|
139
|
+
"Add missing viewBox attribute"
|
|
140
|
+
when :style_violation
|
|
141
|
+
"Promote style properties to attributes"
|
|
142
|
+
when :structural_violation
|
|
143
|
+
"Manual fix required - document structure issue"
|
|
144
|
+
else
|
|
145
|
+
"Apply available remediation"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Check if this issue affects document content
|
|
150
|
+
def affects_content?
|
|
151
|
+
case @violation_type
|
|
152
|
+
when :content_violation, :reference_violation
|
|
153
|
+
true
|
|
154
|
+
when :color_violation, :font_violation, :style_violation, :viewbox_violation, :namespace_violation
|
|
155
|
+
false
|
|
156
|
+
else
|
|
157
|
+
false
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_h
|
|
162
|
+
{
|
|
163
|
+
type: @type,
|
|
164
|
+
rule: rule_id,
|
|
165
|
+
message: @message,
|
|
166
|
+
line: line,
|
|
167
|
+
column: column,
|
|
168
|
+
element: element_name,
|
|
169
|
+
fixable: fixable?,
|
|
170
|
+
remediable: remediable?,
|
|
171
|
+
violation_type: @violation_type,
|
|
172
|
+
remediation_type: remediation_type,
|
|
173
|
+
remediation_confidence: remediation_confidence,
|
|
174
|
+
suggested_action: suggested_action,
|
|
175
|
+
affects_content: affects_content?,
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def to_s
|
|
180
|
+
location = line ? " at line #{line}" : ""
|
|
181
|
+
location += ":#{column}" if column
|
|
182
|
+
rule_info = rule_id ? " (#{rule_id})" : ""
|
|
183
|
+
remediation_info = remediable? ? " [#{remediation_type}]" : " [NOT REMEDIABLE]"
|
|
184
|
+
"#{@message}#{location}#{rule_info}#{remediation_info}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def detect_violation_type
|
|
190
|
+
req_id = requirement_id.downcase
|
|
191
|
+
msg = @message.downcase
|
|
192
|
+
|
|
193
|
+
# First check for structural violations (non-remediable)
|
|
194
|
+
return :structural_violation if msg.include?("root element must be") ||
|
|
195
|
+
msg.include?("malformed") ||
|
|
196
|
+
msg.include?("invalid document") ||
|
|
197
|
+
msg.include?("required element missing") ||
|
|
198
|
+
msg.include?("invalid hierarchy")
|
|
199
|
+
|
|
200
|
+
# Then check requirement-based violations (remediable)
|
|
201
|
+
case req_id
|
|
202
|
+
when /color/
|
|
203
|
+
:color_violation
|
|
204
|
+
when /font/
|
|
205
|
+
:font_violation
|
|
206
|
+
when /forbidden|content/
|
|
207
|
+
:content_violation
|
|
208
|
+
when /reference|id/
|
|
209
|
+
:reference_violation
|
|
210
|
+
when /namespace/
|
|
211
|
+
:namespace_violation
|
|
212
|
+
when /viewbox/
|
|
213
|
+
:viewbox_violation
|
|
214
|
+
when /style/
|
|
215
|
+
:style_violation
|
|
216
|
+
else
|
|
217
|
+
# Check message content for clues
|
|
218
|
+
return :color_violation if msg.include?("color")
|
|
219
|
+
return :font_violation if msg.include?("font")
|
|
220
|
+
return :content_violation if msg.include?("forbidden")
|
|
221
|
+
return :reference_violation if msg.include?("reference") || msg.include?("href")
|
|
222
|
+
return :namespace_violation if msg.include?("namespace")
|
|
223
|
+
return :viewbox_violation if msg.include?("viewbox")
|
|
224
|
+
return :style_violation if msg.include?("style")
|
|
225
|
+
|
|
226
|
+
:unknown_violation
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def rule_id
|
|
231
|
+
return @requirement_id_override if @requirement_id_override
|
|
232
|
+
return @rule.id if @rule.respond_to?(:id)
|
|
233
|
+
|
|
234
|
+
if @rule.respond_to?(:class) && @rule.class.respond_to?(:name)
|
|
235
|
+
# Extract ID from class name for requirements
|
|
236
|
+
class_name = @rule.class.name.split("::").last
|
|
237
|
+
class_name.gsub(/Requirement$/, "").downcase.gsub(/([a-z])([A-Z])/,
|
|
238
|
+
'\1_\2').downcase
|
|
239
|
+
else
|
|
240
|
+
"unknown"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Errors
|
|
7
|
+
# Validation notice (informational message, not an error)
|
|
8
|
+
#
|
|
9
|
+
# Represents informational notices during validation, such as
|
|
10
|
+
# external reference notices or other non-error messages.
|
|
11
|
+
class ValidationNotice < Base
|
|
12
|
+
attr_reader :data
|
|
13
|
+
|
|
14
|
+
def initialize(type:, node:, message:, data: {})
|
|
15
|
+
super(type: type, node: node, message: message)
|
|
16
|
+
@data = data
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
type: @type,
|
|
22
|
+
message: @message,
|
|
23
|
+
line: line,
|
|
24
|
+
column: column,
|
|
25
|
+
data: @data,
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SvgConform
|
|
4
|
+
module Interfaces
|
|
5
|
+
# Defines the contract for validation requirements
|
|
6
|
+
#
|
|
7
|
+
# All requirements must implement the methods defined in this interface.
|
|
8
|
+
# This ensures consistent behavior between DOM and SAX validation modes.
|
|
9
|
+
#
|
|
10
|
+
# @example Implementing a requirement
|
|
11
|
+
# class MyRequirement < BaseRequirement
|
|
12
|
+
# include RequirementInterface
|
|
13
|
+
#
|
|
14
|
+
# def check(node, context)
|
|
15
|
+
# # DOM validation logic here
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# def validate_sax_element(element, context)
|
|
19
|
+
# # SAX validation logic here
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
module RequirementInterface
|
|
23
|
+
# Required Methods
|
|
24
|
+
# These methods must be implemented by all requirement classes
|
|
25
|
+
|
|
26
|
+
# Validates a single node during DOM traversal
|
|
27
|
+
#
|
|
28
|
+
# This is the main validation method called for each node that passes
|
|
29
|
+
# the should_check_node? filter during DOM-based validation.
|
|
30
|
+
#
|
|
31
|
+
# @param node [Moxml::Node, Nokogiri::XML::Node] The node to validate
|
|
32
|
+
# @param context [ValidationContext] The validation context for reporting errors
|
|
33
|
+
# @return [void]
|
|
34
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
35
|
+
#
|
|
36
|
+
# @example Basic implementation
|
|
37
|
+
# def check(node, context)
|
|
38
|
+
# if invalid_condition?(node)
|
|
39
|
+
# context.add_error(
|
|
40
|
+
# requirement_id: id,
|
|
41
|
+
# message: "Node violates requirement",
|
|
42
|
+
# node: node,
|
|
43
|
+
# severity: :error,
|
|
44
|
+
# )
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
def check(node, context)
|
|
48
|
+
raise NotImplementedError, "#{self.class} must implement #check"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Optional Methods
|
|
52
|
+
# These methods have default implementations but can be overridden
|
|
53
|
+
|
|
54
|
+
# Validates the entire document (called once per requirement)
|
|
55
|
+
#
|
|
56
|
+
# Default implementation traverses the document and calls #check
|
|
57
|
+
# on each node that passes should_check_node?. Override for custom
|
|
58
|
+
# document-level validation logic.
|
|
59
|
+
#
|
|
60
|
+
# @param document [Document] The document to validate
|
|
61
|
+
# @param context [ValidationContext] The validation context for reporting errors
|
|
62
|
+
# @return [void]
|
|
63
|
+
def validate_document(document, context)
|
|
64
|
+
document.traverse do |node|
|
|
65
|
+
check(node, context) if should_check_node?(node, context)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validates an element during SAX parsing
|
|
70
|
+
#
|
|
71
|
+
# Called for each element during streaming SAX validation.
|
|
72
|
+
# Override this method to implement SAX-compatible validation logic.
|
|
73
|
+
#
|
|
74
|
+
# IMPORTANT: In SAX mode, the element is an ElementProxy (not a full DOM node).
|
|
75
|
+
# It provides: name, attributes, parent, path, position but NOT:
|
|
76
|
+
# - children (not yet parsed)
|
|
77
|
+
# - tree traversal (impossible during streaming)
|
|
78
|
+
# - XPath queries (not available)
|
|
79
|
+
#
|
|
80
|
+
# Use ElementProxy methods:
|
|
81
|
+
# - element.name - element tag name
|
|
82
|
+
# - element.attributes - hash of attributes
|
|
83
|
+
# - element.parent - parent ElementProxy
|
|
84
|
+
# - element.path - array of element names representing path
|
|
85
|
+
# - element.position - 1-based position in document
|
|
86
|
+
# - element.path_id - unique node ID for tracking
|
|
87
|
+
#
|
|
88
|
+
# @param element [ElementProxy] The element being validated
|
|
89
|
+
# @param context [ValidationContext] The validation context for reporting errors
|
|
90
|
+
# @return [void]
|
|
91
|
+
def validate_sax_element(element, context)
|
|
92
|
+
# Default: Empty - subclasses must override for SAX support
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Collects data during SAX parsing for deferred validation
|
|
96
|
+
#
|
|
97
|
+
# Called for each element during SAX parsing to collect data
|
|
98
|
+
# that will be used later in validate_sax_complete.
|
|
99
|
+
#
|
|
100
|
+
# Use this for requirements that need to collect references, IDs,
|
|
101
|
+
# or other data before validating (e.g., ID/reference validation).
|
|
102
|
+
#
|
|
103
|
+
# @param element [ElementProxy] The element being examined
|
|
104
|
+
# @param context [ValidationContext] The validation context
|
|
105
|
+
# @return [void]
|
|
106
|
+
def collect_sax_data(element, context)
|
|
107
|
+
# Default: no data collection
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Performs deferred validation after document is fully parsed
|
|
111
|
+
#
|
|
112
|
+
# Called once after SAX parsing completes. Use this to validate
|
|
113
|
+
# relationships that require forward references (e.g., ID references,
|
|
114
|
+
# cross-element constraints).
|
|
115
|
+
#
|
|
116
|
+
# Override this method and return true from needs_deferred_validation?
|
|
117
|
+
# to enable deferred validation for this requirement.
|
|
118
|
+
#
|
|
119
|
+
# @param context [ValidationContext] The validation context containing
|
|
120
|
+
# collected data and for reporting errors
|
|
121
|
+
# @return [void]
|
|
122
|
+
def validate_sax_complete(context)
|
|
123
|
+
# Default: no deferred validation
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Indicates if this requirement needs deferred validation
|
|
127
|
+
#
|
|
128
|
+
# Return true if this requirement needs to validate after the full
|
|
129
|
+
# document is parsed (e.g., for ID/reference validation).
|
|
130
|
+
#
|
|
131
|
+
# When returning true, also implement validate_sax_complete.
|
|
132
|
+
#
|
|
133
|
+
# @return [Boolean] true if deferred validation is needed
|
|
134
|
+
def needs_deferred_validation?
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Determines if this requirement should check a specific node
|
|
139
|
+
#
|
|
140
|
+
# Return false to skip validation for a node. The default implementation
|
|
141
|
+
# skips nodes that are not elements (text nodes, comments, etc.) and
|
|
142
|
+
# structurally invalid nodes.
|
|
143
|
+
#
|
|
144
|
+
# Override this to add additional filtering logic.
|
|
145
|
+
#
|
|
146
|
+
# @param node [Moxml::Node, Nokogiri::XML::Node, ElementProxy] The node to check
|
|
147
|
+
# @param context [ValidationContext, nil] The validation context (may be nil in some cases)
|
|
148
|
+
# @return [Boolean] true if the node should be validated
|
|
149
|
+
def should_check_node?(node, context = nil)
|
|
150
|
+
return false unless node.respond_to?(:name) && node.respond_to?(:attributes)
|
|
151
|
+
|
|
152
|
+
# Skip structurally invalid nodes
|
|
153
|
+
return false if context&.node_structurally_invalid?(node)
|
|
154
|
+
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Resets any state between validations (for batch mode)
|
|
159
|
+
#
|
|
160
|
+
# Called between validations when validating multiple files.
|
|
161
|
+
# Override this method if your requirement maintains state that
|
|
162
|
+
# needs to be reset.
|
|
163
|
+
#
|
|
164
|
+
# @return [void]
|
|
165
|
+
def reset_state
|
|
166
|
+
# Default: no state to reset
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Returns a string representation of the requirement
|
|
170
|
+
#
|
|
171
|
+
# @return [String] The requirement ID and description
|
|
172
|
+
def to_s
|
|
173
|
+
"#{@id}: #{@description}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -177,7 +177,8 @@ module SvgConform
|
|
|
177
177
|
# Store the prefixes to remove on the document for later serialization
|
|
178
178
|
# Try to store on Document wrapper first, fallback to moxml_document
|
|
179
179
|
target = document.respond_to?(:instance_variable_set) ? document : root.document
|
|
180
|
-
target&.instance_variable_set(:@unused_namespace_prefixes,
|
|
180
|
+
target&.instance_variable_set(:@unused_namespace_prefixes,
|
|
181
|
+
disallowed_prefixes)
|
|
181
182
|
end
|
|
182
183
|
|
|
183
184
|
def find_used_namespace_prefixes(document)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../node_helpers"
|
|
4
|
+
require_relative "../interfaces/requirement_interface"
|
|
4
5
|
|
|
5
6
|
module SvgConform
|
|
6
7
|
module Requirements
|
|
7
8
|
# Base class for all validation requirements
|
|
8
9
|
class BaseRequirement < Lutaml::Model::Serializable
|
|
9
10
|
include SvgConform::NodeHelpers
|
|
11
|
+
include SvgConform::Interfaces::RequirementInterface
|
|
10
12
|
|
|
11
13
|
attribute :id, :string
|
|
12
14
|
attribute :description, :string
|
|
@@ -81,6 +83,20 @@ module SvgConform
|
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
|
|
86
|
+
# Shared helper for skipping attribute validation on structurally invalid nodes
|
|
87
|
+
# Used by both DOM and SAX validation
|
|
88
|
+
def skip_attribute_validation?(node_or_element, context)
|
|
89
|
+
# For DOM mode, also check if it's an element
|
|
90
|
+
return true if element?(node_or_element) && context.node_structurally_invalid?(node_or_element)
|
|
91
|
+
|
|
92
|
+
# For SAX mode (ElementProxy always has this method)
|
|
93
|
+
if node_or_element.respond_to?(:path_id)
|
|
94
|
+
return context.node_structurally_invalid?(node_or_element)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
def to_s
|
|
85
101
|
"#{@id}: #{@description}"
|
|
86
102
|
end
|
|
@@ -26,70 +26,30 @@ module SvgConform
|
|
|
26
26
|
def check(node, context)
|
|
27
27
|
return unless element?(node)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
return if context.node_structurally_invalid?(node)
|
|
31
|
-
|
|
32
|
-
# Check color-related attributes
|
|
33
|
-
color_attributes = %w[fill stroke color stop-color flood-color
|
|
34
|
-
lighting-color]
|
|
35
|
-
|
|
36
|
-
color_attributes.each do |attr_name|
|
|
37
|
-
value = get_attribute(node, attr_name)
|
|
38
|
-
next if value.nil? || value.empty?
|
|
39
|
-
|
|
40
|
-
next if valid_color?(value)
|
|
41
|
-
|
|
42
|
-
context.add_error(
|
|
43
|
-
requirement_id: id,
|
|
44
|
-
message: "Color '#{value}' in attribute '#{attr_name}' is not allowed in this profile",
|
|
45
|
-
node: node,
|
|
46
|
-
severity: :error,
|
|
47
|
-
data: {
|
|
48
|
-
attribute: attr_name,
|
|
49
|
-
value: value,
|
|
50
|
-
element: node.name,
|
|
51
|
-
},
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Check style attribute for color properties
|
|
56
|
-
style_value = get_attribute(node, "style")
|
|
57
|
-
return unless style_value
|
|
58
|
-
|
|
59
|
-
styles = parse_style(style_value)
|
|
60
|
-
color_properties = %w[fill stroke color stop-color flood-color
|
|
61
|
-
lighting-color]
|
|
62
|
-
|
|
63
|
-
color_properties.each do |prop|
|
|
64
|
-
value = styles[prop]
|
|
65
|
-
next if value.nil? || value.empty?
|
|
66
|
-
|
|
67
|
-
next if valid_color?(value)
|
|
68
|
-
|
|
69
|
-
context.add_error(
|
|
70
|
-
requirement_id: id,
|
|
71
|
-
message: "Color '#{value}' in style property '#{prop}' is not allowed in this profile",
|
|
72
|
-
node: node,
|
|
73
|
-
severity: :error,
|
|
74
|
-
data: {
|
|
75
|
-
attribute: prop,
|
|
76
|
-
value: value,
|
|
77
|
-
element: node.name,
|
|
78
|
-
},
|
|
79
|
-
)
|
|
80
|
-
end
|
|
29
|
+
validate_colors(node, context)
|
|
81
30
|
end
|
|
82
31
|
|
|
83
32
|
def validate_sax_element(element, context)
|
|
33
|
+
# ElementProxy always represents an element, so no element? check needed
|
|
34
|
+
validate_colors(element, context)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Shared validation logic for both DOM and SAX modes
|
|
40
|
+
def validate_colors(node_or_element, context)
|
|
84
41
|
# Skip attribute validation for structurally invalid nodes
|
|
85
|
-
return if
|
|
42
|
+
return if skip_attribute_validation?(node_or_element, context)
|
|
43
|
+
|
|
44
|
+
# Get element name
|
|
45
|
+
element_name = node_or_element.name
|
|
86
46
|
|
|
87
47
|
# Check color-related attributes
|
|
88
48
|
color_attributes = %w[fill stroke color stop-color flood-color
|
|
89
49
|
lighting-color]
|
|
90
50
|
|
|
91
51
|
color_attributes.each do |attr_name|
|
|
92
|
-
value =
|
|
52
|
+
value = get_attribute(node_or_element, attr_name)
|
|
93
53
|
next if value.nil? || value.empty?
|
|
94
54
|
|
|
95
55
|
next if valid_color?(value)
|
|
@@ -97,18 +57,18 @@ module SvgConform
|
|
|
97
57
|
context.add_error(
|
|
98
58
|
requirement_id: id,
|
|
99
59
|
message: "Color '#{value}' in attribute '#{attr_name}' is not allowed in this profile",
|
|
100
|
-
node:
|
|
60
|
+
node: node_or_element,
|
|
101
61
|
severity: :error,
|
|
102
62
|
data: {
|
|
103
63
|
attribute: attr_name,
|
|
104
64
|
value: value,
|
|
105
|
-
element:
|
|
65
|
+
element: element_name,
|
|
106
66
|
},
|
|
107
67
|
)
|
|
108
68
|
end
|
|
109
69
|
|
|
110
70
|
# Check style attribute for color properties
|
|
111
|
-
style_value =
|
|
71
|
+
style_value = get_attribute(node_or_element, "style")
|
|
112
72
|
return unless style_value
|
|
113
73
|
|
|
114
74
|
styles = parse_style(style_value)
|
|
@@ -124,19 +84,17 @@ module SvgConform
|
|
|
124
84
|
context.add_error(
|
|
125
85
|
requirement_id: id,
|
|
126
86
|
message: "Color '#{value}' in style property '#{prop}' is not allowed in this profile",
|
|
127
|
-
node:
|
|
87
|
+
node: node_or_element,
|
|
128
88
|
severity: :error,
|
|
129
89
|
data: {
|
|
130
90
|
attribute: prop,
|
|
131
91
|
value: value,
|
|
132
|
-
element:
|
|
92
|
+
element: element_name,
|
|
133
93
|
},
|
|
134
94
|
)
|
|
135
95
|
end
|
|
136
96
|
end
|
|
137
97
|
|
|
138
|
-
private
|
|
139
|
-
|
|
140
98
|
def valid_color?(color)
|
|
141
99
|
# First check if threshold-based validation is enabled
|
|
142
100
|
if black_and_white_threshold
|