svg_conform 0.1.1 → 0.1.2
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 +273 -10
- data/README.adoc +7 -8
- data/examples/readme_usage.rb +67 -0
- data/examples/requirements_demo.rb +4 -4
- data/lib/svg_conform/document.rb +7 -1
- data/lib/svg_conform/element_proxy.rb +101 -0
- data/lib/svg_conform/fast_document_analyzer.rb +82 -0
- data/lib/svg_conform/node_index_builder.rb +47 -0
- data/lib/svg_conform/requirements/allowed_elements_requirement.rb +202 -0
- data/lib/svg_conform/requirements/base_requirement.rb +27 -0
- data/lib/svg_conform/requirements/color_restrictions_requirement.rb +53 -0
- data/lib/svg_conform/requirements/font_family_requirement.rb +18 -0
- data/lib/svg_conform/requirements/forbidden_content_requirement.rb +26 -0
- data/lib/svg_conform/requirements/id_reference_requirement.rb +96 -0
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +91 -0
- data/lib/svg_conform/requirements/link_validation_requirement.rb +30 -0
- data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +59 -0
- data/lib/svg_conform/requirements/namespace_requirement.rb +74 -0
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +74 -0
- data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +58 -0
- data/lib/svg_conform/requirements/no_external_images_requirement.rb +40 -0
- data/lib/svg_conform/requirements/style_requirement.rb +12 -0
- data/lib/svg_conform/requirements/viewbox_required_requirement.rb +72 -0
- data/lib/svg_conform/sax_document.rb +46 -0
- data/lib/svg_conform/sax_validation_handler.rb +158 -0
- data/lib/svg_conform/validation_context.rb +84 -2
- data/lib/svg_conform/validator.rb +74 -6
- data/lib/svg_conform/version.rb +1 -1
- data/lib/svg_conform.rb +1 -0
- metadata +8 -2
|
@@ -17,6 +17,8 @@ module SvgConform
|
|
|
17
17
|
@infos = []
|
|
18
18
|
@data = {}
|
|
19
19
|
@structurally_invalid_node_ids = Set.new
|
|
20
|
+
@node_id_cache = {}
|
|
21
|
+
@cache_populated = false
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
# Mark a node as structurally invalid (e.g., invalid parent-child relationship)
|
|
@@ -24,6 +26,8 @@ module SvgConform
|
|
|
24
26
|
# Also marks all descendants as invalid since they'll be removed with the parent
|
|
25
27
|
def mark_node_structurally_invalid(node)
|
|
26
28
|
node_id = generate_node_id(node)
|
|
29
|
+
return if node_id.nil? # Safety check
|
|
30
|
+
|
|
27
31
|
@structurally_invalid_node_ids.add(node_id)
|
|
28
32
|
|
|
29
33
|
# Mark all descendants as invalid too
|
|
@@ -36,6 +40,8 @@ module SvgConform
|
|
|
36
40
|
|
|
37
41
|
node.children.each do |child|
|
|
38
42
|
child_id = generate_node_id(child)
|
|
43
|
+
return if child_id.nil? # Safety check
|
|
44
|
+
|
|
39
45
|
@structurally_invalid_node_ids.add(child_id)
|
|
40
46
|
# Recursively mark descendants
|
|
41
47
|
mark_descendants_invalid(child)
|
|
@@ -45,6 +51,8 @@ module SvgConform
|
|
|
45
51
|
# Check if a node is structurally invalid
|
|
46
52
|
def node_structurally_invalid?(node)
|
|
47
53
|
node_id = generate_node_id(node)
|
|
54
|
+
return false if node_id.nil? # Safety check
|
|
55
|
+
|
|
48
56
|
@structurally_invalid_node_ids.include?(node_id)
|
|
49
57
|
end
|
|
50
58
|
|
|
@@ -120,16 +128,90 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
|
|
|
120
128
|
|
|
121
129
|
# Generate a unique identifier for a node based on its path
|
|
122
130
|
# Builds a stable path by walking up the parent chain
|
|
131
|
+
# OPTIMIZED: Lazy cache population - populate entire cache on first call
|
|
123
132
|
def generate_node_id(node)
|
|
124
133
|
return nil unless node.respond_to?(:name)
|
|
125
134
|
|
|
126
|
-
#
|
|
135
|
+
# Populate cache for ALL nodes on first access
|
|
136
|
+
unless @cache_populated
|
|
137
|
+
populate_node_id_cache
|
|
138
|
+
@cache_populated = true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Try cache lookup first
|
|
142
|
+
cached_id = @node_id_cache[node]
|
|
143
|
+
return cached_id if cached_id
|
|
144
|
+
|
|
145
|
+
# Fall back to building path if node not in cache
|
|
146
|
+
# (happens when different traversals create different wrapper objects)
|
|
147
|
+
build_node_path(node)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# Populate cache for all nodes using document.traverse with parent tracking
|
|
153
|
+
def populate_node_id_cache
|
|
154
|
+
parent_stack = []
|
|
155
|
+
counter_stack = [{}] # Stack of {element_name => count} hashes
|
|
156
|
+
|
|
157
|
+
@document.traverse do |node|
|
|
158
|
+
next unless node.respond_to?(:name) && node.name
|
|
159
|
+
|
|
160
|
+
# Detect parent changes by checking node.parent
|
|
161
|
+
current_parent = node.respond_to?(:parent) ? node.parent : nil
|
|
162
|
+
|
|
163
|
+
# Adjust stack based on actual parent
|
|
164
|
+
while parent_stack.size > 0 && !parent_stack.last.equal?(current_parent)
|
|
165
|
+
parent_stack.pop
|
|
166
|
+
counter_stack.pop
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# If we have a new parent level, push it
|
|
170
|
+
if current_parent && (parent_stack.empty? || !parent_stack.last.equal?(current_parent))
|
|
171
|
+
parent_stack.push(current_parent)
|
|
172
|
+
counter_stack.push({})
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Increment counter at current level
|
|
176
|
+
current_counters = counter_stack.last || {}
|
|
177
|
+
current_counters[node.name] ||= 0
|
|
178
|
+
current_counters[node.name] += 1
|
|
179
|
+
|
|
180
|
+
# Build path using original backward logic (for correctness)
|
|
181
|
+
@node_id_cache[node] = build_node_path(node)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Traverse tree, building paths with forward position counters
|
|
186
|
+
def traverse_with_forward_counting(node, path_parts, sibling_counters)
|
|
187
|
+
return unless node.respond_to?(:name) && node.name
|
|
188
|
+
|
|
189
|
+
# Increment counter for this node name at current level
|
|
190
|
+
sibling_counters[node.name] ||= 0
|
|
191
|
+
sibling_counters[node.name] += 1
|
|
192
|
+
position = sibling_counters[node.name]
|
|
193
|
+
|
|
194
|
+
# Build and cache path
|
|
195
|
+
current_path = path_parts + ["#{node.name}[#{position}]"]
|
|
196
|
+
@node_id_cache[node] = "/#{current_path.join('/')}"
|
|
197
|
+
|
|
198
|
+
# Traverse children with fresh counters
|
|
199
|
+
if node.respond_to?(:children)
|
|
200
|
+
child_counters = {}
|
|
201
|
+
node.children.each do |child|
|
|
202
|
+
traverse_with_forward_counting(child, current_path, child_counters)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Build path-based ID for a node (original logic, unchanged)
|
|
208
|
+
def build_node_path(node)
|
|
127
209
|
path_parts = []
|
|
128
210
|
current = node
|
|
129
211
|
|
|
130
212
|
while current
|
|
131
213
|
if current.respond_to?(:name) && current.name
|
|
132
|
-
# Count previous siblings of the same type for position
|
|
214
|
+
# Count previous siblings of the same type for position (ORIGINAL LOGIC)
|
|
133
215
|
position = 1
|
|
134
216
|
if current.respond_to?(:previous_sibling)
|
|
135
217
|
sibling = current.previous_sibling
|
|
@@ -9,6 +9,7 @@ module SvgConform
|
|
|
9
9
|
@options = {
|
|
10
10
|
fix: false,
|
|
11
11
|
strict: false,
|
|
12
|
+
mode: :auto # :auto, :dom, or :sax
|
|
12
13
|
}.merge(options)
|
|
13
14
|
end
|
|
14
15
|
|
|
@@ -19,17 +20,31 @@ module SvgConform
|
|
|
19
20
|
"File not found: #{file_path}"
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
merged_options = @options.merge(options)
|
|
24
|
+
mode = determine_mode(file_path, merged_options[:mode])
|
|
25
|
+
|
|
26
|
+
case mode
|
|
27
|
+
when :sax
|
|
28
|
+
validate_file_sax(file_path, profile: profile, **merged_options)
|
|
29
|
+
when :dom
|
|
30
|
+
validate_file_dom(file_path, profile: profile, **merged_options)
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
# Validate SVG content string
|
|
27
35
|
def validate(svg_content, profile: :svg_1_2_rfc, **options)
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
merged_options = @options.merge(options)
|
|
37
|
+
mode = merged_options[:mode] == :sax ? :sax : :dom
|
|
38
|
+
|
|
39
|
+
case mode
|
|
40
|
+
when :sax
|
|
41
|
+
validate_content_sax(svg_content, profile: profile, **merged_options)
|
|
42
|
+
when :dom
|
|
43
|
+
validate_content_dom(svg_content, profile: profile, **merged_options)
|
|
44
|
+
end
|
|
30
45
|
end
|
|
31
46
|
|
|
32
|
-
# Validate a Document object
|
|
47
|
+
# Validate a Document object (DOM only)
|
|
33
48
|
def validate_document(document, profile: :svg_1_2_rfc, **options)
|
|
34
49
|
merged_options = @options.merge(options)
|
|
35
50
|
profile_obj = resolve_profile(profile)
|
|
@@ -64,6 +79,59 @@ module SvgConform
|
|
|
64
79
|
|
|
65
80
|
private
|
|
66
81
|
|
|
82
|
+
def determine_mode(file_path, requested_mode)
|
|
83
|
+
case requested_mode
|
|
84
|
+
when :sax
|
|
85
|
+
:sax
|
|
86
|
+
when :dom
|
|
87
|
+
:dom
|
|
88
|
+
when :auto
|
|
89
|
+
# Use SAX for files larger than 1MB
|
|
90
|
+
file_size = File.size(file_path)
|
|
91
|
+
file_size > 1_000_000 ? :sax : :dom
|
|
92
|
+
else
|
|
93
|
+
:dom
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_file_sax(file_path, profile:, **options)
|
|
98
|
+
profile_obj = resolve_profile(profile)
|
|
99
|
+
sax_doc = SvgConform::SaxDocument.from_file(file_path)
|
|
100
|
+
result = sax_doc.validate_with_profile(profile_obj)
|
|
101
|
+
|
|
102
|
+
# If fixing is requested, convert to DOM and apply fixes
|
|
103
|
+
if options[:fix] && result.has_errors?
|
|
104
|
+
dom_doc = SvgConform::Document.from_file(file_path)
|
|
105
|
+
result = validate_document(dom_doc, profile: profile_obj, **options)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_file_dom(file_path, profile:, **options)
|
|
112
|
+
document = SvgConform::Document.from_file(file_path)
|
|
113
|
+
validate_document(document, profile: profile, **options)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_content_sax(content, profile:, **options)
|
|
117
|
+
profile_obj = resolve_profile(profile)
|
|
118
|
+
sax_doc = SvgConform::SaxDocument.from_content(content)
|
|
119
|
+
result = sax_doc.validate_with_profile(profile_obj)
|
|
120
|
+
|
|
121
|
+
# If fixing is requested, convert to DOM and apply fixes
|
|
122
|
+
if options[:fix] && result.has_errors?
|
|
123
|
+
dom_doc = SvgConform::Document.from_content(content)
|
|
124
|
+
result = validate_document(dom_doc, profile: profile_obj, **options)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_content_dom(content, profile:, **options)
|
|
131
|
+
document = SvgConform::Document.from_content(content)
|
|
132
|
+
validate_document(document, profile: profile, **options)
|
|
133
|
+
end
|
|
134
|
+
|
|
67
135
|
def resolve_profile(profile)
|
|
68
136
|
case profile
|
|
69
137
|
when Symbol, String
|
|
@@ -84,7 +152,7 @@ module SvgConform
|
|
|
84
152
|
warnings: [],
|
|
85
153
|
file_path: file_path,
|
|
86
154
|
error?: true,
|
|
87
|
-
to_s: -> { "Error processing #{file_path}: #{error.message}" }
|
|
155
|
+
to_s: -> { "Error processing #{file_path}: #{error.message}" }
|
|
88
156
|
)
|
|
89
157
|
end
|
|
90
158
|
end
|
data/lib/svg_conform/version.rb
CHANGED
data/lib/svg_conform.rb
CHANGED
|
@@ -11,6 +11,7 @@ end
|
|
|
11
11
|
|
|
12
12
|
require_relative "svg_conform/version"
|
|
13
13
|
require_relative "svg_conform/document"
|
|
14
|
+
require_relative "svg_conform/sax_document"
|
|
14
15
|
require_relative "svg_conform/validation_context"
|
|
15
16
|
require_relative "svg_conform/validation_result"
|
|
16
17
|
require_relative "svg_conform/profiles"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: svg_conform
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- docs/remediation.adoc
|
|
98
98
|
- docs/requirements.adoc
|
|
99
99
|
- examples/demo.rb
|
|
100
|
+
- examples/readme_usage.rb
|
|
100
101
|
- examples/requirements_demo.rb
|
|
101
102
|
- exe/svg_conform
|
|
102
103
|
- lib/svg_conform.rb
|
|
@@ -122,6 +123,7 @@ files:
|
|
|
122
123
|
- lib/svg_conform/constants.rb
|
|
123
124
|
- lib/svg_conform/css_color.rb
|
|
124
125
|
- lib/svg_conform/document.rb
|
|
126
|
+
- lib/svg_conform/element_proxy.rb
|
|
125
127
|
- lib/svg_conform/external_checkers.rb
|
|
126
128
|
- lib/svg_conform/external_checkers/svgcheck.rb
|
|
127
129
|
- lib/svg_conform/external_checkers/svgcheck/compatibility_engine.rb
|
|
@@ -130,7 +132,9 @@ files:
|
|
|
130
132
|
- lib/svg_conform/external_checkers/svgcheck/report_comparator.rb
|
|
131
133
|
- lib/svg_conform/external_checkers/svgcheck/report_generator.rb
|
|
132
134
|
- lib/svg_conform/external_checkers/svgcheck/validation_pipeline.rb
|
|
135
|
+
- lib/svg_conform/fast_document_analyzer.rb
|
|
133
136
|
- lib/svg_conform/fixer.rb
|
|
137
|
+
- lib/svg_conform/node_index_builder.rb
|
|
134
138
|
- lib/svg_conform/profile.rb
|
|
135
139
|
- lib/svg_conform/profiles.rb
|
|
136
140
|
- lib/svg_conform/remediation_engine.rb
|
|
@@ -167,6 +171,8 @@ files:
|
|
|
167
171
|
- lib/svg_conform/requirements/style_promotion_requirement.rb
|
|
168
172
|
- lib/svg_conform/requirements/style_requirement.rb
|
|
169
173
|
- lib/svg_conform/requirements/viewbox_required_requirement.rb
|
|
174
|
+
- lib/svg_conform/sax_document.rb
|
|
175
|
+
- lib/svg_conform/sax_validation_handler.rb
|
|
170
176
|
- lib/svg_conform/semantic_comparator.rb
|
|
171
177
|
- lib/svg_conform/validation_context.rb
|
|
172
178
|
- lib/svg_conform/validation_result.rb
|