svg_conform 0.1.2 → 0.1.4

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +17 -239
  3. data/README.adoc +55 -0
  4. data/lib/svg_conform/element_proxy.rb +10 -10
  5. data/lib/svg_conform/fast_document_analyzer.rb +22 -22
  6. data/lib/svg_conform/node_index_builder.rb +1 -0
  7. data/lib/svg_conform/profile.rb +23 -8
  8. data/lib/svg_conform/requirements/allowed_elements_requirement.rb +29 -9
  9. data/lib/svg_conform/requirements/color_restrictions_requirement.rb +8 -6
  10. data/lib/svg_conform/requirements/font_family_requirement.rb +6 -2
  11. data/lib/svg_conform/requirements/forbidden_content_requirement.rb +2 -2
  12. data/lib/svg_conform/requirements/id_reference_requirement.rb +10 -7
  13. data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +7 -6
  14. data/lib/svg_conform/requirements/link_validation_requirement.rb +2 -2
  15. data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +16 -10
  16. data/lib/svg_conform/requirements/namespace_requirement.rb +11 -10
  17. data/lib/svg_conform/requirements/no_external_css_requirement.rb +32 -5
  18. data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +2 -2
  19. data/lib/svg_conform/requirements/no_external_images_requirement.rb +2 -2
  20. data/lib/svg_conform/requirements/style_promotion_requirement.rb +7 -0
  21. data/lib/svg_conform/requirements/viewbox_required_requirement.rb +7 -7
  22. data/lib/svg_conform/sax_document.rb +2 -2
  23. data/lib/svg_conform/sax_validation_handler.rb +14 -10
  24. data/lib/svg_conform/validation_context.rb +9 -7
  25. data/lib/svg_conform/validator.rb +24 -5
  26. data/lib/svg_conform/version.rb +1 -1
  27. data/spec/spec_helper.rb +3 -0
  28. data/spec/support/shared_examples_for_validation_modes.rb +71 -0
  29. metadata +3 -2
@@ -13,11 +13,11 @@ module SvgConform
13
13
 
14
14
  def initialize(profile)
15
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
16
+ @element_stack = [] # Track parent-child hierarchy
17
+ @path_stack = [] # Current element path
18
+ @position_counters = [] # Stack of sibling counters per level
19
19
  @parse_errors = []
20
- @result = nil # Will be set in end_document
20
+ @result = nil # Will be set in end_document
21
21
 
22
22
  # Create validation context (without document reference for SAX)
23
23
  @context = create_sax_context
@@ -50,13 +50,13 @@ module SvgConform
50
50
  attributes: attrs,
51
51
  position: position,
52
52
  path: @path_stack.dup,
53
- parent: @element_stack.last
53
+ parent: @element_stack.last,
54
54
  )
55
55
 
56
56
  # Push to stacks
57
57
  @element_stack.push(element)
58
58
  @path_stack.push("#{name}[#{position}]")
59
- @position_counters.push({}) # New level for this element's children
59
+ @position_counters.push({}) # New level for this element's children
60
60
 
61
61
  # Validate with immediate requirements
62
62
  @immediate_requirements.each do |req|
@@ -65,12 +65,15 @@ module SvgConform
65
65
 
66
66
  # Deferred requirements may need to collect data
67
67
  @deferred_requirements.each do |req|
68
- req.collect_sax_data(element, @context) if req.respond_to?(:collect_sax_data)
68
+ if req.respond_to?(:collect_sax_data)
69
+ req.collect_sax_data(element,
70
+ @context)
71
+ end
69
72
  end
70
73
  end
71
74
 
72
75
  # SAX Event: Element end tag
73
- def end_element(name)
76
+ def end_element(_name)
74
77
  @element_stack.pop
75
78
  @path_stack.pop
76
79
  @position_counters.pop
@@ -79,6 +82,7 @@ module SvgConform
79
82
  # SAX Event: Text content
80
83
  def characters(string)
81
84
  return if @element_stack.empty?
85
+
82
86
  @element_stack.last.text_content << string
83
87
  end
84
88
 
@@ -109,7 +113,7 @@ module SvgConform
109
113
  node: nil,
110
114
  message: "Parse error: #{error.message}",
111
115
  requirement_id: "parse_error",
112
- severity: :error
116
+ severity: :error,
113
117
  )
114
118
  end
115
119
 
@@ -138,7 +142,7 @@ module SvgConform
138
142
  context.instance_variable_set(:@data, {})
139
143
  context.instance_variable_set(:@structurally_invalid_node_ids, Set.new)
140
144
  context.instance_variable_set(:@node_id_cache, {})
141
- context.instance_variable_set(:@cache_populated, true) # Skip population for SAX
145
+ context.instance_variable_set(:@cache_populated, true) # Skip population for SAX
142
146
  context
143
147
  end
144
148
 
@@ -26,7 +26,7 @@ module SvgConform
26
26
  # Also marks all descendants as invalid since they'll be removed with the parent
27
27
  def mark_node_structurally_invalid(node)
28
28
  node_id = generate_node_id(node)
29
- return if node_id.nil? # Safety check
29
+ return if node_id.nil? # Safety check
30
30
 
31
31
  @structurally_invalid_node_ids.add(node_id)
32
32
 
@@ -36,11 +36,13 @@ module SvgConform
36
36
 
37
37
  # Mark all descendants of a node as structurally invalid
38
38
  def mark_descendants_invalid(node)
39
- return unless node.respond_to?(:children)
39
+ # In SAX mode, ElementProxy doesn't have children yet
40
+ # Children will be validated individually and will check parent validity
41
+ return unless node.respond_to?(:children) && node.children
40
42
 
41
43
  node.children.each do |child|
42
44
  child_id = generate_node_id(child)
43
- return if child_id.nil? # Safety check
45
+ next if child_id.nil? # Skip if can't generate ID
44
46
 
45
47
  @structurally_invalid_node_ids.add(child_id)
46
48
  # Recursively mark descendants
@@ -51,7 +53,7 @@ module SvgConform
51
53
  # Check if a node is structurally invalid
52
54
  def node_structurally_invalid?(node)
53
55
  node_id = generate_node_id(node)
54
- return false if node_id.nil? # Safety check
56
+ return false if node_id.nil? # Safety check
55
57
 
56
58
  @structurally_invalid_node_ids.include?(node_id)
57
59
  end
@@ -152,7 +154,7 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
152
154
  # Populate cache for all nodes using document.traverse with parent tracking
153
155
  def populate_node_id_cache
154
156
  parent_stack = []
155
- counter_stack = [{}] # Stack of {element_name => count} hashes
157
+ counter_stack = [{}] # Stack of {element_name => count} hashes
156
158
 
157
159
  @document.traverse do |node|
158
160
  next unless node.respond_to?(:name) && node.name
@@ -161,7 +163,7 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
161
163
  current_parent = node.respond_to?(:parent) ? node.parent : nil
162
164
 
163
165
  # Adjust stack based on actual parent
164
- while parent_stack.size > 0 && !parent_stack.last.equal?(current_parent)
166
+ while parent_stack.size.positive? && !parent_stack.last.equal?(current_parent)
165
167
  parent_stack.pop
166
168
  counter_stack.pop
167
169
  end
@@ -187,7 +189,7 @@ requirement_id: nil, severity: nil, fix: nil, data: {})
187
189
  return unless node.respond_to?(:name) && node.name
188
190
 
189
191
  # Increment counter for this node name at current level
190
- sibling_counters[node.name] ||= 0
192
+ sibling_counters[node.name] ||= 0
191
193
  sibling_counters[node.name] += 1
192
194
  position = sibling_counters[node.name]
193
195
 
@@ -9,7 +9,7 @@ module SvgConform
9
9
  @options = {
10
10
  fix: false,
11
11
  strict: false,
12
- mode: :auto # :auto, :dom, or :sax
12
+ mode: :sax, # Testing SAX mode
13
13
  }.merge(options)
14
14
  end
15
15
 
@@ -34,13 +34,21 @@ module SvgConform
34
34
  # Validate SVG content string
35
35
  def validate(svg_content, profile: :svg_1_2_rfc, **options)
36
36
  merged_options = @options.merge(options)
37
- mode = merged_options[:mode] == :sax ? :sax : :dom
37
+ mode = merged_options[:mode]
38
+
39
+ # Default to SAX if not specified
40
+ mode = :sax if mode.nil?
38
41
 
39
42
  case mode
40
43
  when :sax
41
44
  validate_content_sax(svg_content, profile: profile, **merged_options)
42
45
  when :dom
43
46
  validate_content_dom(svg_content, profile: profile, **merged_options)
47
+ when :auto
48
+ # For content, we can't check file size, so default to SAX
49
+ validate_content_sax(svg_content, profile: profile, **merged_options)
50
+ else
51
+ validate_content_sax(svg_content, profile: profile, **merged_options)
44
52
  end
45
53
  end
46
54
 
@@ -99,10 +107,21 @@ module SvgConform
99
107
  sax_doc = SvgConform::SaxDocument.from_file(file_path)
100
108
  result = sax_doc.validate_with_profile(profile_obj)
101
109
 
102
- # If fixing is requested, convert to DOM and apply fixes
110
+ # If fixing is requested, load DOM and apply remediations directly
111
+ # Skip re-validation to avoid DOM performance penalty
103
112
  if options[:fix] && result.has_errors?
104
113
  dom_doc = SvgConform::Document.from_file(file_path)
105
- result = validate_document(dom_doc, profile: profile_obj, **options)
114
+
115
+ # Apply remediations directly without re-validating
116
+ changes = profile_obj.apply_remediations(dom_doc)
117
+
118
+ # Write fixed output if specified
119
+ if options[:fix_output] && changes.any?
120
+ File.write(options[:fix_output], dom_doc.to_xml)
121
+ end
122
+
123
+ # Return original SAX validation result (errors already detected)
124
+ # Note: We don't re-validate to avoid DOM performance cost
106
125
  end
107
126
 
108
127
  result
@@ -152,7 +171,7 @@ module SvgConform
152
171
  warnings: [],
153
172
  file_path: file_path,
154
173
  error?: true,
155
- to_s: -> { "Error processing #{file_path}: #{error.message}" }
174
+ to_s: -> { "Error processing #{file_path}: #{error.message}" },
156
175
  )
157
176
  end
158
177
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SvgConform
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  require "svg_conform"
4
4
  require "canon"
5
5
 
6
+ # Load shared examples
7
+ Dir[File.expand_path("support/**/*.rb", __dir__)].each { |f| require f }
8
+
6
9
  RSpec.configure do |config|
7
10
  # Enable flags like --only-failures and --next-failure
8
11
  config.example_status_persistence_file_path = ".rspec_status"
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared examples for testing both DOM and SAX validation modes
4
+ RSpec.shared_examples "validates in both DOM and SAX modes" do |file_path, profile_name|
5
+ let(:file_path) { file_path }
6
+ let(:profile) { profile_name }
7
+
8
+ it "produces identical results in DOM and SAX modes" do
9
+ dom_result = SvgConform::Validator.new(mode: :dom).validate_file(file_path,
10
+ profile: profile)
11
+ sax_result = SvgConform::Validator.new(mode: :sax).validate_file(file_path,
12
+ profile: profile)
13
+
14
+ expect(sax_result.errors.size).to eq(dom_result.errors.size),
15
+ "SAX mode should produce same error count as DOM mode (DOM: #{dom_result.errors.size}, SAX: #{sax_result.errors.size})"
16
+
17
+ expect(sax_result.warnings.size).to eq(dom_result.warnings.size),
18
+ "SAX mode should produce same warning count as DOM mode"
19
+
20
+ expect(sax_result.valid?).to eq(dom_result.valid?),
21
+ "SAX mode should have same validity as DOM mode"
22
+ end
23
+
24
+ it "DOM mode validates correctly" do
25
+ result = SvgConform::Validator.new(mode: :dom).validate_file(file_path,
26
+ profile: profile)
27
+ expect(result).to be_a(SvgConform::ValidationResult)
28
+ end
29
+
30
+ it "SAX mode validates correctly" do
31
+ result = SvgConform::Validator.new(mode: :sax).validate_file(file_path,
32
+ profile: profile)
33
+ expect(result).to be_a(SvgConform::ValidationResult)
34
+ end
35
+
36
+ describe "performance comparison" do
37
+ it "SAX mode is faster than or equal to DOM mode" do
38
+ skip "Performance test - run manually for large files"
39
+
40
+ dom_time = Benchmark.realtime do
41
+ SvgConform::Validator.new(mode: :dom).validate_file(file_path,
42
+ profile: profile)
43
+ end
44
+
45
+ sax_time = Benchmark.realtime do
46
+ SvgConform::Validator.new(mode: :sax).validate_file(file_path,
47
+ profile: profile)
48
+ end
49
+
50
+ expect(sax_time).to be <= dom_time
51
+ end
52
+ end
53
+ end
54
+
55
+ RSpec.shared_examples "validates content in both modes" do |svg_content, profile_name|
56
+ let(:content) { svg_content }
57
+ let(:profile) { profile_name }
58
+
59
+ it "produces identical results for content validation" do
60
+ dom_result = SvgConform::Validator.new(mode: :dom).validate(content,
61
+ profile: profile)
62
+ sax_result = SvgConform::Validator.new(mode: :sax).validate(content,
63
+ profile: profile)
64
+
65
+ expect(sax_result.errors.size).to eq(dom_result.errors.size),
66
+ "SAX mode should produce same error count as DOM mode (DOM: #{dom_result.errors.size}, SAX: #{sax_result.errors.size})"
67
+
68
+ expect(sax_result.valid?).to eq(dom_result.valid?),
69
+ "SAX mode should have same validity as DOM mode"
70
+ end
71
+ end
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.2
4
+ version: 0.1.4
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-18 00:00:00.000000000 Z
11
+ date: 2025-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -372,6 +372,7 @@ files:
372
372
  - spec/fixtures/viewbox_required/inputs/missing_viewbox.svg
373
373
  - spec/fixtures/viewbox_required/repair/missing_viewbox.svg
374
374
  - spec/spec_helper.rb
375
+ - spec/support/shared_examples_for_validation_modes.rb
375
376
  - spec/svg_conform/batch_report_spec.rb
376
377
  - spec/svg_conform/commands/check_command_spec.rb
377
378
  - spec/svg_conform/commands/profiles_command_spec.rb