svg_conform 0.1.0 → 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/.github/workflows/rake.yml +4 -1
- data/.github/workflows/release.yml +6 -2
- data/.rubocop_todo.yml +273 -10
- data/Gemfile +1 -0
- data/README.adoc +54 -37
- data/config/profiles/metanorma.yml +4 -4
- data/docs/remediation.adoc +541 -542
- data/docs/requirements.adoc +800 -357
- 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/remediations/no_external_css_remediation.rb +4 -4
- 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
- data/spec/fixtures/namespace/repair/basic_violations.svg +3 -3
- data/spec/fixtures/namespace_attributes/repair/basic_violations.svg +2 -2
- data/spec/fixtures/no_external_css/repair/basic_violations.svg +2 -2
- data/spec/fixtures/style_promotion/repair/basic_test.svg +2 -2
- data/svg_conform.gemspec +1 -1
- metadata +12 -6
|
@@ -83,6 +83,78 @@ module SvgConform
|
|
|
83
83
|
)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
def validate_sax_element(element, context)
|
|
87
|
+
# Only check the root SVG element
|
|
88
|
+
return unless element.name == "svg" && element.parent.nil?
|
|
89
|
+
|
|
90
|
+
viewbox = element.raw_attributes["viewBox"]
|
|
91
|
+
|
|
92
|
+
if viewbox.nil? || viewbox.empty?
|
|
93
|
+
context.add_error(
|
|
94
|
+
requirement_id: id,
|
|
95
|
+
message: "SVG root element must have a viewBox attribute",
|
|
96
|
+
node: element,
|
|
97
|
+
severity: :error,
|
|
98
|
+
data: { missing_attribute: "viewBox" }
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Add informational message about calculated viewBox if width/height are present
|
|
102
|
+
width = element.raw_attributes["width"]
|
|
103
|
+
height = element.raw_attributes["height"]
|
|
104
|
+
|
|
105
|
+
if width && height && valid_number?(width) && valid_number?(height)
|
|
106
|
+
calculated_viewbox = "0 0 #{width.to_f} #{height.to_f}"
|
|
107
|
+
context.add_error(
|
|
108
|
+
requirement_id: id,
|
|
109
|
+
message: "Trying to put in the attribute with value '#{calculated_viewbox}'",
|
|
110
|
+
node: element,
|
|
111
|
+
severity: :error,
|
|
112
|
+
data: {
|
|
113
|
+
calculated_viewbox: calculated_viewbox,
|
|
114
|
+
source_width: width,
|
|
115
|
+
source_height: height
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Validate viewBox format (should be "min-x min-y width height")
|
|
123
|
+
normalized_viewbox = viewbox.strip.gsub(/[(),]/, " ").squeeze(" ")
|
|
124
|
+
parts = normalized_viewbox.strip.split(/\s+/)
|
|
125
|
+
unless parts.length == 4 && parts.all? { |part| valid_number?(part) }
|
|
126
|
+
context.add_error(
|
|
127
|
+
requirement_id: id,
|
|
128
|
+
message: "viewBox attribute must contain four numeric values (min-x min-y width height)",
|
|
129
|
+
node: element,
|
|
130
|
+
severity: :error,
|
|
131
|
+
data: {
|
|
132
|
+
viewbox_value: viewbox,
|
|
133
|
+
parsed_parts: parts
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check that width and height are positive
|
|
140
|
+
width = parts[2].to_f
|
|
141
|
+
height = parts[3].to_f
|
|
142
|
+
|
|
143
|
+
return unless width <= 0 || height <= 0
|
|
144
|
+
|
|
145
|
+
context.add_error(
|
|
146
|
+
requirement_id: id,
|
|
147
|
+
message: "viewBox width and height must be positive values",
|
|
148
|
+
node: element,
|
|
149
|
+
severity: :error,
|
|
150
|
+
data: {
|
|
151
|
+
viewbox_value: viewbox,
|
|
152
|
+
width: width,
|
|
153
|
+
height: height
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
86
158
|
private
|
|
87
159
|
|
|
88
160
|
def valid_number?(str)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require_relative "sax_validation_handler"
|
|
5
|
+
|
|
6
|
+
module SvgConform
|
|
7
|
+
# SAX-based document for streaming validation
|
|
8
|
+
# Provides high-performance validation for large SVG files
|
|
9
|
+
# without loading entire DOM tree into memory
|
|
10
|
+
class SaxDocument
|
|
11
|
+
attr_reader :file_path, :content
|
|
12
|
+
|
|
13
|
+
def self.from_file(file_path)
|
|
14
|
+
new(File.read(file_path), file_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_content(content)
|
|
18
|
+
new(content, nil)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(content, file_path = nil)
|
|
22
|
+
@content = content
|
|
23
|
+
@file_path = file_path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Validate using SAX streaming parser
|
|
27
|
+
def validate_with_profile(profile)
|
|
28
|
+
handler = SaxValidationHandler.new(profile)
|
|
29
|
+
parser = Nokogiri::XML::SAX::Parser.new(handler)
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
parser.parse(@content)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
# Handle parse errors
|
|
35
|
+
handler.add_parse_error(e)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
handler.result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# For compatibility - convert to DOM when needed
|
|
42
|
+
def to_dom
|
|
43
|
+
Document.from_content(@content)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require_relative "element_proxy"
|
|
5
|
+
require_relative "validation_context"
|
|
6
|
+
require_relative "validation_result"
|
|
7
|
+
|
|
8
|
+
module SvgConform
|
|
9
|
+
# SAX event handler for streaming SVG validation
|
|
10
|
+
# Processes XML events and dispatches to requirements
|
|
11
|
+
class SaxValidationHandler < Nokogiri::XML::SAX::Document
|
|
12
|
+
attr_reader :result, :context
|
|
13
|
+
|
|
14
|
+
def initialize(profile)
|
|
15
|
+
@profile = profile
|
|
16
|
+
@element_stack = [] # Track parent-child hierarchy
|
|
17
|
+
@path_stack = [] # Current element path
|
|
18
|
+
@position_counters = [] # Stack of sibling counters per level
|
|
19
|
+
@parse_errors = []
|
|
20
|
+
@result = nil # Will be set in end_document
|
|
21
|
+
|
|
22
|
+
# Create validation context (without document reference for SAX)
|
|
23
|
+
@context = create_sax_context
|
|
24
|
+
|
|
25
|
+
# Classify requirements into immediate vs deferred
|
|
26
|
+
@immediate_requirements = []
|
|
27
|
+
@deferred_requirements = []
|
|
28
|
+
classify_requirements
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# SAX Event: Document start
|
|
32
|
+
def start_document
|
|
33
|
+
# Initialize root level counters
|
|
34
|
+
@position_counters.push({})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# SAX Event: Element start tag
|
|
38
|
+
def start_element(name, attributes = [])
|
|
39
|
+
attrs = Hash[attributes]
|
|
40
|
+
|
|
41
|
+
# Calculate position among siblings at current level
|
|
42
|
+
current_counters = @position_counters.last || {}
|
|
43
|
+
current_counters[name] ||= 0
|
|
44
|
+
current_counters[name] += 1
|
|
45
|
+
position = current_counters[name]
|
|
46
|
+
|
|
47
|
+
# Build element proxy
|
|
48
|
+
element = ElementProxy.new(
|
|
49
|
+
name: name,
|
|
50
|
+
attributes: attrs,
|
|
51
|
+
position: position,
|
|
52
|
+
path: @path_stack.dup,
|
|
53
|
+
parent: @element_stack.last
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Push to stacks
|
|
57
|
+
@element_stack.push(element)
|
|
58
|
+
@path_stack.push("#{name}[#{position}]")
|
|
59
|
+
@position_counters.push({}) # New level for this element's children
|
|
60
|
+
|
|
61
|
+
# Validate with immediate requirements
|
|
62
|
+
@immediate_requirements.each do |req|
|
|
63
|
+
req.validate_sax_element(element, @context)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Deferred requirements may need to collect data
|
|
67
|
+
@deferred_requirements.each do |req|
|
|
68
|
+
req.collect_sax_data(element, @context) if req.respond_to?(:collect_sax_data)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# SAX Event: Element end tag
|
|
73
|
+
def end_element(name)
|
|
74
|
+
@element_stack.pop
|
|
75
|
+
@path_stack.pop
|
|
76
|
+
@position_counters.pop
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# SAX Event: Text content
|
|
80
|
+
def characters(string)
|
|
81
|
+
return if @element_stack.empty?
|
|
82
|
+
@element_stack.last.text_content << string
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# SAX Event: Document complete
|
|
86
|
+
def end_document
|
|
87
|
+
# Run deferred validation
|
|
88
|
+
@deferred_requirements.each do |req|
|
|
89
|
+
req.validate_sax_complete(@context)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Create result
|
|
93
|
+
@result = ValidationResult.new(nil, @profile, @context)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# SAX Event: Parse error
|
|
97
|
+
def error(error_message)
|
|
98
|
+
@parse_errors << error_message
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# SAX Event: Warning
|
|
102
|
+
def warning(warning_message)
|
|
103
|
+
# Can log warnings if needed
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Handle parse errors
|
|
107
|
+
def add_parse_error(error)
|
|
108
|
+
@context.add_error(
|
|
109
|
+
node: nil,
|
|
110
|
+
message: "Parse error: #{error.message}",
|
|
111
|
+
requirement_id: "parse_error",
|
|
112
|
+
severity: :error
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get result (will be nil until end_document called)
|
|
117
|
+
def result
|
|
118
|
+
@result || create_incomplete_result
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def create_incomplete_result
|
|
124
|
+
# Return result even if parsing incomplete
|
|
125
|
+
ValidationResult.new(nil, @profile, @context)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Create a SAX-compatible validation context
|
|
129
|
+
def create_sax_context
|
|
130
|
+
# Create context without triggering DOM operations
|
|
131
|
+
context = ValidationContext.allocate
|
|
132
|
+
context.instance_variable_set(:@document, nil)
|
|
133
|
+
context.instance_variable_set(:@profile, @profile)
|
|
134
|
+
context.instance_variable_set(:@errors, [])
|
|
135
|
+
context.instance_variable_set(:@warnings, [])
|
|
136
|
+
context.instance_variable_set(:@validity_errors, [])
|
|
137
|
+
context.instance_variable_set(:@infos, [])
|
|
138
|
+
context.instance_variable_set(:@data, {})
|
|
139
|
+
context.instance_variable_set(:@structurally_invalid_node_ids, Set.new)
|
|
140
|
+
context.instance_variable_set(:@node_id_cache, {})
|
|
141
|
+
context.instance_variable_set(:@cache_populated, true) # Skip population for SAX
|
|
142
|
+
context
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Classify requirements based on validation needs
|
|
146
|
+
def classify_requirements
|
|
147
|
+
return unless @profile.requirements
|
|
148
|
+
|
|
149
|
+
@profile.requirements.each do |req|
|
|
150
|
+
if req.respond_to?(:needs_deferred_validation?) && req.needs_deferred_validation?
|
|
151
|
+
@deferred_requirements << req
|
|
152
|
+
else
|
|
153
|
+
@immediate_requirements << req
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -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"
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
version="1.2" baseProfile="tiny" width="100" height="100" viewBox="0 0 100 100">
|
|
4
4
|
<title>Namespace Violations</title>
|
|
5
5
|
|
|
6
|
-
<!-- Invalid namespace elements
|
|
6
|
+
<!-- Invalid namespace elements -->
|
|
7
7
|
|
|
8
|
-
<!-- Invalid namespace attributes
|
|
8
|
+
<!-- Invalid namespace attributes on valid elements -->
|
|
9
9
|
<rect x="10" y="10" width="20" height="20"/>
|
|
10
10
|
<g>
|
|
11
11
|
<circle cx="50" cy="50" r="15"/>
|
|
12
12
|
</g>
|
|
13
13
|
|
|
14
|
-
<!-- Valid namespaces
|
|
14
|
+
<!-- Valid namespaces -->
|
|
15
15
|
<use xlink:href="#test" x="10" y="10"/>
|
|
16
16
|
<text xml:space="preserve" x="10" y="90">Valid</text>
|
|
17
17
|
</svg>
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
version="1.2" baseProfile="tiny" width="100" height="100" viewBox="0 0 100 100">
|
|
4
4
|
<title>Namespace Attributes Test</title>
|
|
5
5
|
|
|
6
|
-
<!-- Invalid namespace attributes
|
|
6
|
+
<!-- Invalid namespace attributes on valid elements -->
|
|
7
7
|
<rect x="10" y="10" width="20" height="20"/>
|
|
8
8
|
<g>
|
|
9
9
|
<circle cx="50" cy="50" r="15"/>
|
|
10
10
|
</g>
|
|
11
11
|
|
|
12
|
-
<!-- Valid namespace attributes
|
|
12
|
+
<!-- Valid namespace attributes -->
|
|
13
13
|
<use xlink:href="#test" x="10" y="10"/>
|
|
14
14
|
<text xml:space="preserve" x="10" y="90">Valid</text>
|
|
15
15
|
</svg>
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
</style>
|
|
11
11
|
</defs>
|
|
12
12
|
|
|
13
|
-
<!-- External stylesheet link
|
|
13
|
+
<!-- External stylesheet link -->
|
|
14
14
|
|
|
15
|
-
<!-- Valid internal styles
|
|
15
|
+
<!-- Valid internal styles -->
|
|
16
16
|
<rect x="10" y="10" width="30" height="30" class="internal"/>
|
|
17
17
|
<circle cx="70" cy="25" r="15" style="fill: white; stroke: black"/>
|
|
18
18
|
</svg>
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
<title>Style Promotion</title>
|
|
5
5
|
<desc>Testing style property promotion to attributes</desc>
|
|
6
6
|
|
|
7
|
-
<!-- Style properties
|
|
7
|
+
<!-- Style properties that should be promoted -->
|
|
8
8
|
<rect x="10" y="10" width="30" height="20" fill="black" stroke="none"/>
|
|
9
9
|
<circle cx="60" cy="20" r="10" fill="white" stroke="black" stroke-width="1"/>
|
|
10
10
|
<text x="10" y="50" font-family="serif" font-size="12" fill="black">Text</text>
|
|
11
11
|
<path d="M 10 60 L 40 70 L 10 70 Z" fill="none" stroke="black" stroke-width="2"/>
|
|
12
12
|
|
|
13
|
-
<!--
|
|
13
|
+
<!-- Mixed style and attribute (style should win) -->
|
|
14
14
|
<rect x="80" y="10" width="20" height="20" fill="black"/>
|
|
15
15
|
</svg>
|
data/svg_conform.gemspec
CHANGED
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
spec.email = ["open.source@ribose.com"]
|
|
14
14
|
|
|
15
15
|
spec.summary = "SVG profile conformance checker for Ruby."
|
|
16
|
-
spec.homepage = "https://github.com/
|
|
16
|
+
spec.homepage = "https://github.com/claricle/svg_conform"
|
|
17
17
|
spec.license = "BSD-2-Clause"
|
|
18
18
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
|
|
19
19
|
|
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
|
+
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
|
|
@@ -407,13 +413,13 @@ files:
|
|
|
407
413
|
- spec/svg_conform_spec.rb
|
|
408
414
|
- spec/svgcheck_compatibility_spec.rb
|
|
409
415
|
- svg_conform.gemspec
|
|
410
|
-
homepage: https://github.com/
|
|
416
|
+
homepage: https://github.com/claricle/svg_conform
|
|
411
417
|
licenses:
|
|
412
418
|
- BSD-2-Clause
|
|
413
419
|
metadata:
|
|
414
|
-
homepage_uri: https://github.com/
|
|
415
|
-
source_code_uri: https://github.com/
|
|
416
|
-
bug_tracker_uri: https://github.com/
|
|
420
|
+
homepage_uri: https://github.com/claricle/svg_conform
|
|
421
|
+
source_code_uri: https://github.com/claricle/svg_conform
|
|
422
|
+
bug_tracker_uri: https://github.com/claricle/svg_conform/issues
|
|
417
423
|
post_install_message:
|
|
418
424
|
rdoc_options: []
|
|
419
425
|
require_paths:
|