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,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_remediation"
|
|
4
|
+
require "set"
|
|
4
5
|
|
|
5
6
|
module SvgConform
|
|
6
7
|
module Remediations
|
|
@@ -105,6 +106,11 @@ module SvgConform
|
|
|
105
106
|
remove_node(node)
|
|
106
107
|
end
|
|
107
108
|
|
|
109
|
+
# Clean up unused namespace declarations
|
|
110
|
+
# Note: Moxml/Nokogiri don't support removing namespace declarations directly,
|
|
111
|
+
# so we mark the document for post-processing during serialization
|
|
112
|
+
cleanup_unused_namespace_declarations(document)
|
|
113
|
+
|
|
108
114
|
changes
|
|
109
115
|
end
|
|
110
116
|
|
|
@@ -146,6 +152,61 @@ module SvgConform
|
|
|
146
152
|
# Same logic as find_namespace_uri_for_prefix
|
|
147
153
|
find_namespace_uri_for_prefix(node, prefix)
|
|
148
154
|
end
|
|
155
|
+
|
|
156
|
+
def cleanup_unused_namespace_declarations(document)
|
|
157
|
+
# Find which namespace prefixes are still in use
|
|
158
|
+
used_prefixes = find_used_namespace_prefixes(document)
|
|
159
|
+
|
|
160
|
+
# Get the root element
|
|
161
|
+
root = document.respond_to?(:root) ? document.root : document
|
|
162
|
+
return unless root.respond_to?(:namespace_definitions)
|
|
163
|
+
|
|
164
|
+
# Get disallowed namespace prefixes
|
|
165
|
+
disallowed_prefixes = root.namespace_definitions.filter_map do |ns|
|
|
166
|
+
prefix = ns.respond_to?(:prefix) ? ns.prefix : nil
|
|
167
|
+
# Extract namespace URI from Moxml::Namespace object
|
|
168
|
+
uri = ns.respond_to?(:uri) ? ns.uri : nil
|
|
169
|
+
next if prefix.nil? || prefix.empty? # Skip default namespace
|
|
170
|
+
next if allowed_namespaces.include?(uri) # Keep allowed namespaces
|
|
171
|
+
|
|
172
|
+
next if used_prefixes.include?(prefix) # Keep prefixes still in use
|
|
173
|
+
|
|
174
|
+
prefix
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Store the prefixes to remove on the document for later serialization
|
|
178
|
+
# Try to store on Document wrapper first, fallback to moxml_document
|
|
179
|
+
target = document.respond_to?(:instance_variable_set) ? document : root.document
|
|
180
|
+
target&.instance_variable_set(:@unused_namespace_prefixes,
|
|
181
|
+
disallowed_prefixes)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def find_used_namespace_prefixes(document)
|
|
185
|
+
used_prefixes = Set.new(["xml"]) # xml prefix is always reserved
|
|
186
|
+
|
|
187
|
+
document.traverse do |node|
|
|
188
|
+
next unless node.respond_to?(:name)
|
|
189
|
+
|
|
190
|
+
# Check element name for namespace prefix
|
|
191
|
+
if node.name.include?(":")
|
|
192
|
+
prefix = node.name.split(":").first
|
|
193
|
+
used_prefixes << prefix
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check attributes for namespace prefixes
|
|
197
|
+
if node.respond_to?(:attributes)
|
|
198
|
+
node.attributes.each do |attr|
|
|
199
|
+
attr_name = attr.respond_to?(:name) ? attr.name : attr.to_s
|
|
200
|
+
if attr_name.include?(":")
|
|
201
|
+
prefix = attr_name.split(":").first
|
|
202
|
+
used_prefixes << prefix
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
used_prefixes
|
|
209
|
+
end
|
|
149
210
|
end
|
|
150
211
|
end
|
|
151
212
|
end
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../node_helpers"
|
|
4
|
+
require_relative "../interfaces/requirement_interface"
|
|
5
|
+
|
|
3
6
|
module SvgConform
|
|
4
7
|
module Requirements
|
|
5
8
|
# Base class for all validation requirements
|
|
6
9
|
class BaseRequirement < Lutaml::Model::Serializable
|
|
10
|
+
include SvgConform::NodeHelpers
|
|
11
|
+
include SvgConform::Interfaces::RequirementInterface
|
|
12
|
+
|
|
7
13
|
attribute :id, :string
|
|
8
14
|
attribute :description, :string
|
|
9
15
|
attribute :type, :string, polymorphic_class: true, default: -> {
|
|
@@ -65,48 +71,8 @@ module SvgConform
|
|
|
65
71
|
true
|
|
66
72
|
end
|
|
67
73
|
|
|
68
|
-
# Helper method to check if a node is an element
|
|
69
|
-
def element?(node)
|
|
70
|
-
node.respond_to?(:name) && !node.name.nil?
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Helper method to check if a node is text
|
|
74
|
-
def text?(node)
|
|
75
|
-
node.respond_to?(:text?) && node.text?
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Helper method to get attribute value
|
|
79
|
-
def get_attribute(node, name)
|
|
80
|
-
return nil unless node.respond_to?(:attribute)
|
|
81
|
-
|
|
82
|
-
attr = node.attribute(name)
|
|
83
|
-
attr&.value
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Helper method to set attribute value
|
|
87
|
-
def set_attribute(node, name, value)
|
|
88
|
-
return false unless node.respond_to?(:set_attribute)
|
|
89
|
-
|
|
90
|
-
node.set_attribute(name, value)
|
|
91
|
-
true
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Helper method to remove attribute
|
|
95
|
-
def remove_attribute(node, name)
|
|
96
|
-
return false unless node.respond_to?(:remove_attribute)
|
|
97
|
-
|
|
98
|
-
node.remove_attribute(name)
|
|
99
|
-
true
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Helper method to check if attribute exists
|
|
103
|
-
def has_attribute?(node, name)
|
|
104
|
-
return false unless node.respond_to?(:attribute)
|
|
105
|
-
|
|
106
|
-
!node.attribute(name).nil?
|
|
107
|
-
end
|
|
108
|
-
|
|
109
74
|
# Helper method to get all attributes
|
|
75
|
+
# Note: Other attribute helpers are included via NodeHelpers module
|
|
110
76
|
def get_attributes(node)
|
|
111
77
|
return {} unless node.respond_to?(:attributes)
|
|
112
78
|
|
|
@@ -117,6 +83,20 @@ module SvgConform
|
|
|
117
83
|
end
|
|
118
84
|
end
|
|
119
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
|
+
|
|
120
100
|
def to_s
|
|
121
101
|
"#{@id}: #{@description}"
|
|
122
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
|
|
@@ -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
|