svg_conform 0.1.10 → 0.1.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f59b89abd58959b19078c28ce450f6ee102a007176d102031a04233f924465e
4
- data.tar.gz: 61e68396baf1673fcd8d764a2d141cd62006d1167d2cea0b9d8695a7bb7ea6c2
3
+ metadata.gz: 9300ab0026e52b929e0707e14c19ce45394da831dfd5e44f4afad14e1a2237cd
4
+ data.tar.gz: b19b0d2dbce1397abb478105704c994a32f418972240c9c149d375d8be519e2c
5
5
  SHA512:
6
- metadata.gz: ffb22ae1a52cbf687b054dbe17ccf8aa44b17bbeeaad5122b57300e9562e73e9cbf005d0c330b2ce13c28a7e6f349ddd191b3d8e8918c6e8745d663debcbec7c
7
- data.tar.gz: dedcb6619d41fc142b0bb6432f15a693b38699ca52b80ba0e255d38a7feca6a86fca6a52daa15f5e694b8f4ca1da323cf67289129305b99a3f15616df336415a
6
+ metadata.gz: d413baac7ebb781837ff482103d751ce3222c170c613707b8411d5c6614dff71ac874802e0e5df14e276c0d56f537d32259787559359dfb954f6e3392088102d
7
+ data.tar.gz: ca1a62500f77534fca87d5b0e0136328ed81b9d9a7a81510e312a3760f31157e7a42af4934bacda3af2d6915b675bb9f4afdba2746332922cb23351bcc47fba2
data/.rubocop_todo.yml CHANGED
@@ -1,11 +1,19 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-01-23 01:37:10 UTC using RuboCop version 1.82.1.
3
+ # on 2026-01-27 00:27:35 UTC using RuboCop version 1.82.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ # Offense count: 1
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
12
+ # SupportedStyles: with_first_argument, with_fixed_indentation
13
+ Layout/ArgumentAlignment:
14
+ Exclude:
15
+ - 'lib/svg_conform/requirements/invalid_id_references_requirement.rb'
16
+
9
17
  # Offense count: 1
10
18
  # This cop supports safe autocorrection (--autocorrect).
11
19
  # Configuration parameters: EnforcedStyleAlignWith.
@@ -14,7 +22,17 @@ Layout/BlockAlignment:
14
22
  Exclude:
15
23
  - 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
16
24
 
17
- # Offense count: 620
25
+ # Offense count: 1
26
+ # This cop supports safe autocorrection (--autocorrect).
27
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
28
+ # SupportedHashRocketStyles: key, separator, table
29
+ # SupportedColonStyles: key, separator, table
30
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
31
+ Layout/HashAlignment:
32
+ Exclude:
33
+ - 'spec/svg_conform/validation_context_spec.rb'
34
+
35
+ # Offense count: 662
18
36
  # This cop supports safe autocorrection (--autocorrect).
19
37
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
20
38
  # URISchemes: http, https
@@ -22,6 +40,14 @@ Layout/LineLength:
22
40
  Enabled: false
23
41
 
24
42
  # Offense count: 2
43
+ # This cop supports safe autocorrection (--autocorrect).
44
+ # Configuration parameters: AllowInHeredoc.
45
+ Layout/TrailingWhitespace:
46
+ Exclude:
47
+ - 'lib/svg_conform/requirements/invalid_id_references_requirement.rb'
48
+ - 'spec/svg_conform/validation_context_spec.rb'
49
+
50
+ # Offense count: 3
25
51
  # Configuration parameters: AllowedMethods.
26
52
  # AllowedMethods: enums
27
53
  Lint/ConstantDefinitionInBlock:
@@ -91,7 +117,7 @@ Metrics/BlockNesting:
91
117
  Metrics/CyclomaticComplexity:
92
118
  Enabled: false
93
119
 
94
- # Offense count: 258
120
+ # Offense count: 259
95
121
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
96
122
  Metrics/MethodLength:
97
123
  Max: 154
@@ -132,20 +158,12 @@ RSpec/DescribeClass:
132
158
  - 'spec/svg_conform/references/integration_spec.rb'
133
159
  - 'spec/svgcheck_compatibility_spec.rb'
134
160
 
135
- # Offense count: 139
161
+ # Offense count: 157
136
162
  # Configuration parameters: CountAsOne.
137
163
  RSpec/ExampleLength:
138
164
  Max: 53
139
165
 
140
- # Offense count: 1
141
- # This cop supports safe autocorrection (--autocorrect).
142
- # Configuration parameters: EnforcedStyle.
143
- # SupportedStyles: implicit, each, example
144
- RSpec/HookArgument:
145
- Exclude:
146
- - 'spec/spec_helper.rb'
147
-
148
- # Offense count: 2
166
+ # Offense count: 3
149
167
  RSpec/LeakyConstantDeclaration:
150
168
  Exclude:
151
169
  - 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
@@ -161,7 +179,7 @@ RSpec/MultipleDescribes:
161
179
  Exclude:
162
180
  - 'spec/svg_conform/batch_report_spec.rb'
163
181
 
164
- # Offense count: 111
182
+ # Offense count: 126
165
183
  RSpec/MultipleExpectations:
166
184
  Max: 8
167
185
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ # Cache for requirement classification results
5
+ # Instance-level (per SaxValidationHandler) to avoid shared mutable state
6
+ # No mutexes needed - each handler has its own cache
7
+ class ClassificationCache
8
+ def initialize
9
+ @cache = {}
10
+ end
11
+
12
+ # Fetch from cache or compute the value
13
+ def fetch(key)
14
+ @cache[key] ||= yield
15
+ end
16
+
17
+ # Clear specific key or entire cache
18
+ def clear(key = nil)
19
+ if key
20
+ @cache.delete(key)
21
+ else
22
+ @cache.clear
23
+ end
24
+ end
25
+
26
+ # Return number of cached entries
27
+ def size
28
+ @cache.size
29
+ end
30
+
31
+ # Check if key exists in cache
32
+ def key?(key)
33
+ @cache.key?(key)
34
+ end
35
+ end
36
+ end
@@ -6,6 +6,9 @@ require_relative "remediations"
6
6
 
7
7
  module SvgConform
8
8
  # Base class for SVG validation profiles using lutaml-model serialization
9
+ #
10
+ # Profile acts as a factory for requirements and remediations.
11
+ # Each validation creates fresh instances to avoid state pollution.
9
12
  class Profile < Lutaml::Model::Serializable
10
13
  PROFILES_DIR = File.expand_path("../../config/profiles", __dir__)
11
14
 
@@ -6,6 +6,89 @@ require_relative "../interfaces/requirement_interface"
6
6
  module SvgConform
7
7
  module Requirements
8
8
  # Base class for all validation requirements
9
+ #
10
+ # == Validation modes
11
+ #
12
+ # Requirements support two validation modes:
13
+ #
14
+ # === DOM validation (remediation mode)
15
+ #
16
+ # Used when the full document is available and remediation is needed.
17
+ # Requirements implement +validate_document+ to traverse the document once.
18
+ #
19
+ # requirement.validate_document(document, context)
20
+ #
21
+ # === SAX validation (streaming mode)
22
+ #
23
+ # Used for memory-efficient streaming validation without loading the full document.
24
+ # Requirements implement either immediate or deferred validation patterns.
25
+ #
26
+ # ==== Immediate validation (14 requirements)
27
+ #
28
+ # Validates as it encounters each node during SAX parsing.
29
+ # No state is needed - validation is complete after +validate_sax_element+ returns.
30
+ #
31
+ # def validate_sax_element(element, context)
32
+ # # Immediate validation logic
33
+ # context.add_error(...) if invalid
34
+ # end
35
+ #
36
+ # Requirements using immediate validation:
37
+ # - AllowedElementsRequirement
38
+ # - FontFamilyRequirement
39
+ # - ColorRestrictionsRequirement
40
+ # - ViewboxRequiredRequirement
41
+ # - NamespaceRequirement
42
+ # - IdCollectionRequirement
43
+ # - StylePromotionRequirement
44
+ # - NoExternalImagesRequirement
45
+ # - NoExternalFontsRequirement
46
+ # - NamespaceAttributesRequirement
47
+ # - ForbiddenContentRequirement
48
+ # - StyleRequirement
49
+ # - LinkValidationRequirement (registers to ReferenceManifest)
50
+ #
51
+ # ==== Deferred validation (3 requirements)
52
+ #
53
+ # Collects data during SAX parsing and validates at document end.
54
+ # Requires a nested State class to store collected data.
55
+ #
56
+ # class State
57
+ # attr_accessor :collected_data
58
+ #
59
+ # def initialize
60
+ # @collected_data = []
61
+ # end
62
+ # end
63
+ #
64
+ # def needs_deferred_validation?
65
+ # true
66
+ # end
67
+ #
68
+ # def collect_sax_data(element, context)
69
+ # state = context.state_for(self)
70
+ # state.collected_data << extract_data(element)
71
+ # end
72
+ #
73
+ # def validate_sax_complete(context)
74
+ # state = context.state_for(self)
75
+ # # Validate collected data
76
+ # end
77
+ #
78
+ # Requirements using deferred validation:
79
+ # - IdReferenceRequirement (needs forward references to IDs)
80
+ # - InvalidIdReferencesRequirement (needs to collect IDs first)
81
+ # - NoExternalCssRequirement (needs to check style elements)
82
+ #
83
+ # == State management
84
+ #
85
+ # Requirements needing deferred validation must define a nested State class.
86
+ # The ValidationContext manages state instances per requirement via +state_for+:
87
+ #
88
+ # state = context.state_for(self) # Returns State instance for this requirement
89
+ #
90
+ # Each validation gets a fresh state instance, preventing state pollution when
91
+ # reusing the same profile for multiple validations.
9
92
  class BaseRequirement < Lutaml::Model::Serializable
10
93
  include SvgConform::NodeHelpers
11
94
  include SvgConform::Interfaces::RequirementInterface
@@ -6,28 +6,29 @@ require "set"
6
6
  module SvgConform
7
7
  module Requirements
8
8
  class IdReferenceRequirement < BaseRequirement
9
- def needs_deferred_validation?
10
- true
9
+ # Nested State class - requirement owns its state structure
10
+ class State
11
+ attr_accessor :collected_ids, :url_refs, :href_refs, :other_refs
12
+
13
+ def initialize
14
+ @collected_ids = Set.new
15
+ @url_refs = []
16
+ @href_refs = []
17
+ @other_refs = []
18
+ end
11
19
  end
12
20
 
13
- # Reset state before each validation run to prevent state leakage
14
- def reset_state
15
- @collected_ids = Set.new
16
- @collected_url_refs = []
17
- @collected_href_refs = []
18
- @collected_other_refs = []
21
+ def needs_deferred_validation?
22
+ true
19
23
  end
20
24
 
21
- def collect_sax_data(element, _context)
22
- # Initialize collections on first call
23
- @collected_ids ||= Set.new
24
- @collected_url_refs ||= []
25
- @collected_href_refs ||= []
26
- @collected_other_refs ||= []
27
-
25
+ def collect_sax_data(element, context)
26
+ state = context.state_for(self)
28
27
  # Collect IDs
29
28
  id_value = element.raw_attributes["id"]
30
- @collected_ids.add(id_value) if id_value && !id_value.empty?
29
+ if id_value && !id_value.empty?
30
+ state.collected_ids.add(id_value)
31
+ end
31
32
 
32
33
  # Collect url() references
33
34
  url_attributes = %w[fill stroke marker-start marker-mid marker-end
@@ -38,7 +39,7 @@ module SvgConform
38
39
 
39
40
  url_refs = extract_url_references(attr_value)
40
41
  url_refs.each do |ref_id|
41
- @collected_url_refs << [element, ref_id, attr_name]
42
+ state.url_refs << [element, ref_id, attr_name]
42
43
  end
43
44
  end
44
45
 
@@ -47,7 +48,7 @@ module SvgConform
47
48
  if style_attr
48
49
  url_refs = extract_url_references(style_attr)
49
50
  url_refs.each do |ref_id|
50
- @collected_url_refs << [element, ref_id, "style"]
51
+ state.url_refs << [element, ref_id, "style"]
51
52
  end
52
53
  end
53
54
 
@@ -55,7 +56,7 @@ module SvgConform
55
56
  href_value = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
56
57
  if href_value&.start_with?("#")
57
58
  ref_id = href_value[1..] # Remove #
58
- @collected_href_refs << [element, ref_id]
59
+ state.href_refs << [element, ref_id]
59
60
  end
60
61
 
61
62
  # Collect other ID references
@@ -69,18 +70,18 @@ module SvgConform
69
70
  ref_ids.each do |ref_id|
70
71
  next if ref_id.empty?
71
72
 
72
- @collected_other_refs << [element, ref_id, attr_name]
73
+ state.other_refs << [element, ref_id, attr_name]
73
74
  end
74
75
  end
75
76
  end
76
77
 
77
78
  def validate_sax_complete(context)
78
- # Guard against nil collections (if collect_sax_data was never called)
79
- return unless @collected_url_refs && @collected_href_refs && @collected_other_refs && @collected_ids
79
+ state = context.state_for(self)
80
+ return unless state.url_refs && state.href_refs && state.other_refs && state.collected_ids
80
81
 
81
82
  # Validate all collected references
82
- @collected_url_refs.each do |element, ref_id, attr_name|
83
- next if @collected_ids.include?(ref_id)
83
+ state.url_refs.each do |element, ref_id, attr_name|
84
+ next if state.collected_ids.include?(ref_id)
84
85
 
85
86
  message = if attr_name == "style"
86
87
  "Reference to undefined ID '#{ref_id}' in style attribute"
@@ -95,8 +96,8 @@ module SvgConform
95
96
  )
96
97
  end
97
98
 
98
- @collected_href_refs.each do |element, ref_id|
99
- next if @collected_ids.include?(ref_id)
99
+ state.href_refs.each do |element, ref_id|
100
+ next if state.collected_ids.include?(ref_id)
100
101
 
101
102
  context.add_error(
102
103
  node: element,
@@ -105,8 +106,8 @@ module SvgConform
105
106
  )
106
107
  end
107
108
 
108
- @collected_other_refs.each do |element, ref_id, attr_name|
109
- next if @collected_ids.include?(ref_id)
109
+ state.other_refs.each do |element, ref_id, attr_name|
110
+ next if state.collected_ids.include?(ref_id)
110
111
 
111
112
  context.add_error(
112
113
  node: element,
@@ -23,31 +23,43 @@ module SvgConform
23
23
 
24
24
  def initialize(*args)
25
25
  super
26
- @collected_ids = Set.new
27
- @use_element_refs = [] # [element, ref_id, href]
28
- @other_refs = [] # [element, ref_id, attr_name, value]
26
+ # No instance state - validation state stored in context
27
+ end
28
+
29
+ # State class for tracking ID references during SAX parsing
30
+ class State
31
+ attr_accessor :collected_ids, :use_element_refs, :other_refs,
32
+ :existing_ids
33
+
34
+ def initialize
35
+ @collected_ids = Set.new
36
+ @use_element_refs = []
37
+ @other_refs = []
38
+ @existing_ids = nil
39
+ end
29
40
  end
30
41
 
31
42
  def needs_deferred_validation?
32
43
  true
33
44
  end
34
45
 
35
- def collect_sax_data(element, _context)
36
- # Initialize collections on first call
37
- @collected_ids ||= Set.new
38
- @use_element_refs ||= []
39
- @other_refs ||= []
46
+ def collect_sax_data(element, context)
47
+ state = context.state_for(self)
40
48
 
41
49
  # Collect IDs
42
50
  id_attr = element.raw_attributes["id"]
43
- @collected_ids.add(id_attr) if id_attr && !id_attr.empty?
51
+ if id_attr && !id_attr.empty?
52
+ state.collected_ids.add(id_attr)
53
+ end
44
54
 
45
55
  # Collect use element references
46
56
  if check_use_elements && element.name == "use"
47
57
  href = element.raw_attributes["xlink:href"] || element.raw_attributes["href"]
48
58
  if href&.start_with?("#")
49
59
  ref_id = href[1..]
50
- @use_element_refs << [element, ref_id, href] unless ref_id.empty?
60
+ unless ref_id.empty?
61
+ state.use_element_refs << [element, ref_id, href]
62
+ end
51
63
  end
52
64
  end
53
65
 
@@ -61,7 +73,7 @@ module SvgConform
61
73
  next unless attr_value&.match?(/^url\(#(.+)\)$/)
62
74
 
63
75
  ref_id = Regexp.last_match(1)
64
- @other_refs << [element, ref_id, attr_name, attr_value]
76
+ state.other_refs << [element, ref_id, attr_name, attr_value]
65
77
  end
66
78
 
67
79
  # Check style attribute
@@ -72,19 +84,23 @@ module SvgConform
72
84
  next unless value&.match?(/^url\(#(.+)\)$/)
73
85
 
74
86
  ref_id = Regexp.last_match(1)
75
- @other_refs << [element, ref_id, "style:#{property}", value]
87
+ state.other_refs << [element, ref_id, "style:#{property}", value]
76
88
  end
77
89
  end
78
90
  end
79
91
  end
80
92
 
81
93
  def validate_sax_complete(context)
82
- # Guard against nil collections
83
- return unless @use_element_refs && @other_refs && @collected_ids
94
+ state = context.state_for(self)
95
+ collected_ids = state.collected_ids
96
+ use_element_refs = state.use_element_refs
97
+ other_refs = state.other_refs
98
+
99
+ return unless collected_ids && use_element_refs && other_refs
84
100
 
85
101
  # Validate use element references
86
- @use_element_refs.each do |element, ref_id, href|
87
- next if @collected_ids.include?(ref_id)
102
+ use_element_refs.each do |element, ref_id, href|
103
+ next if collected_ids.include?(ref_id)
88
104
 
89
105
  context.add_error(
90
106
  requirement_id: id,
@@ -96,8 +112,8 @@ module SvgConform
96
112
  end
97
113
 
98
114
  # Validate other references if enabled
99
- @other_refs.each do |element, ref_id, attr_name, value|
100
- next if @collected_ids.include?(ref_id)
115
+ other_refs.each do |element, ref_id, attr_name, value|
116
+ next if collected_ids.include?(ref_id)
101
117
 
102
118
  message = if attr_name.start_with?("style:")
103
119
  property = attr_name.split(":", 2)[1]
@@ -119,7 +135,8 @@ module SvgConform
119
135
  def validate_document(document, context)
120
136
  # Collect all existing IDs in the document
121
137
  existing_ids = collect_existing_ids(document)
122
- context.set_data(:existing_ids, existing_ids)
138
+ state = context.state_for(self)
139
+ state.existing_ids = existing_ids
123
140
 
124
141
  # Check for invalid references
125
142
  super(document, context)
@@ -152,13 +169,14 @@ module SvgConform
152
169
  end
153
170
 
154
171
  def check_use_element(node, context)
172
+ state = context.state_for(self)
155
173
  href = get_attribute(node, "xlink:href") || get_attribute(node, "href")
156
174
  return unless href&.start_with?("#")
157
175
 
158
176
  id_ref = href[1..] # Remove # prefix
159
177
  return if id_ref.empty?
160
178
 
161
- existing_ids = context.get_data(:existing_ids)
179
+ existing_ids = state.existing_ids
162
180
  return if existing_ids.include?(id_ref)
163
181
 
164
182
  context.add_error(
@@ -170,13 +188,15 @@ module SvgConform
170
188
  end
171
189
 
172
190
  def check_other_id_references(node, context)
191
+ state = context.state_for(self)
192
+
173
193
  # Check other attributes that reference IDs
174
194
  id_reference_attributes = %w[
175
195
  clip-path mask filter marker-start marker-mid marker-end
176
196
  fill stroke
177
197
  ]
178
198
 
179
- existing_ids = context.get_data(:existing_ids)
199
+ existing_ids = state.existing_ids
180
200
 
181
201
  id_reference_attributes.each do |attr_name|
182
202
  attr_value = get_attribute(node, attr_name)
@@ -22,9 +22,17 @@ module SvgConform
22
22
  map "allowed_protocols", to: :allowed_protocols
23
23
  end
24
24
 
25
+ class State
26
+ attr_accessor :collected_styles
27
+
28
+ def initialize
29
+ @collected_styles = []
30
+ end
31
+ end
32
+
25
33
  def initialize(*args)
26
34
  super
27
- @collected_style_elements = []
35
+ # No instance state - validation state stored in context
28
36
  end
29
37
 
30
38
  def check(node, context)
@@ -44,24 +52,23 @@ module SvgConform
44
52
  check_style_elements # Only deferred if checking style elements
45
53
  end
46
54
 
47
- def collect_sax_data(element, _context)
55
+ def collect_sax_data(element, context)
56
+ state = context.state_for(self)
48
57
  # Collect style elements for deferred validation (text content needs to be complete)
49
58
  if check_style_elements && element.name == "style"
50
- @collected_style_elements << element
59
+ state.collected_styles << element
51
60
  end
52
61
  end
53
62
 
54
63
  def validate_sax_complete(context)
55
- # Guard against nil collection
56
- return unless @collected_style_elements
64
+ state = context.state_for(self)
65
+ collected_style_elements = state.collected_styles
66
+ return unless collected_style_elements
57
67
 
58
68
  # Validate collected style elements
59
- @collected_style_elements.each do |element|
69
+ collected_style_elements.each do |element|
60
70
  check_style_element(element, context)
61
71
  end
62
-
63
- # Reset for next validation
64
- @collected_style_elements = []
65
72
  end
66
73
 
67
74
  def should_check_node?(node, context = nil)