svg_conform 0.1.9 → 0.1.10

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: e4879f5648a06585ca68ae1430f928c327b267c3de96e92ae54333a002127243
4
- data.tar.gz: 7fb876639e3d0d935020089981803d43e2648131bf7a074b0b127605c99009ff
3
+ metadata.gz: 8f59b89abd58959b19078c28ce450f6ee102a007176d102031a04233f924465e
4
+ data.tar.gz: 61e68396baf1673fcd8d764a2d141cd62006d1167d2cea0b9d8695a7bb7ea6c2
5
5
  SHA512:
6
- metadata.gz: ca32f36fb1bcb98fc25be20fc237291f5fa0efedd55c71cb62b8862e60c3628f05673a0d16def9deda237e4a3b53312c78e39d3daf2bb42700e58f388a2d38c8
7
- data.tar.gz: '0086cffd30f8c81aba4e157caf6a0bfe197f4ce597971941e4ddcc136f0ac7cf11460eb8e4103094a6636aa546c5d687c0f724697e441c509a94e1697bc41857'
6
+ metadata.gz: ffb22ae1a52cbf687b054dbe17ccf8aa44b17bbeeaad5122b57300e9562e73e9cbf005d0c330b2ce13c28a7e6f349ddd191b3d8e8918c6e8745d663debcbec7c
7
+ data.tar.gz: dedcb6619d41fc142b0bb6432f15a693b38699ca52b80ba0e255d38a7feca6a86fca6a52daa15f5e694b8f4ca1da323cf67289129305b99a3f15616df336415a
data/.gitignore CHANGED
@@ -10,3 +10,7 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  svgcheck-reference/
13
+
14
+ .kilocode
15
+ .claude
16
+
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-01-22 08:33:31 UTC using RuboCop version 1.82.1.
3
+ # on 2026-01-23 01:37:10 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
@@ -14,7 +14,7 @@ Layout/BlockAlignment:
14
14
  Exclude:
15
15
  - 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
16
16
 
17
- # Offense count: 647
17
+ # Offense count: 620
18
18
  # This cop supports safe autocorrection (--autocorrect).
19
19
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
20
20
  # URISchemes: http, https
@@ -70,7 +70,7 @@ Lint/UnreachableCode:
70
70
  Exclude:
71
71
  - 'lib/svg_conform/commands/check.rb'
72
72
 
73
- # Offense count: 151
73
+ # Offense count: 146
74
74
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
75
75
  Metrics/AbcSize:
76
76
  Enabled: false
@@ -81,17 +81,17 @@ Metrics/AbcSize:
81
81
  Metrics/BlockLength:
82
82
  Max: 253
83
83
 
84
- # Offense count: 6
84
+ # Offense count: 2
85
85
  # Configuration parameters: CountBlocks, CountModifierForms.
86
86
  Metrics/BlockNesting:
87
87
  Max: 4
88
88
 
89
- # Offense count: 127
89
+ # Offense count: 125
90
90
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
91
91
  Metrics/CyclomaticComplexity:
92
92
  Enabled: false
93
93
 
94
- # Offense count: 263
94
+ # Offense count: 258
95
95
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
96
96
  Metrics/MethodLength:
97
97
  Max: 154
@@ -101,18 +101,11 @@ Metrics/MethodLength:
101
101
  Metrics/ParameterLists:
102
102
  Max: 9
103
103
 
104
- # Offense count: 101
104
+ # Offense count: 99
105
105
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
106
106
  Metrics/PerceivedComplexity:
107
107
  Enabled: false
108
108
 
109
- # Offense count: 4
110
- # Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns.
111
- # SupportedStyles: snake_case, camelCase
112
- Naming/VariableName:
113
- Exclude:
114
- - 'check_clippath_font.rb'
115
-
116
109
  # Offense count: 2
117
110
  # Configuration parameters: MinSize.
118
111
  Performance/CollectionLiteralInLoop:
@@ -144,6 +137,14 @@ RSpec/DescribeClass:
144
137
  RSpec/ExampleLength:
145
138
  Max: 53
146
139
 
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
+
147
148
  # Offense count: 2
148
149
  RSpec/LeakyConstantDeclaration:
149
150
  Exclude:
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Performance benchmark for svg_conform optimizations
5
+ # Measures validation speed for single and batch operations
6
+
7
+ require "bundler/setup"
8
+ require "benchmark"
9
+ require_relative "../lib/svg_conform"
10
+
11
+ # Helper method to create test SVG content
12
+ def create_test_svg(element_count: 10, attribute_count: 5)
13
+ elements = (1..element_count).map do |i|
14
+ attrs = (1..attribute_count).map { |j| "attr#{j}='value#{j}'" }.join(" ")
15
+ "<rect id='rect#{i}' x='#{i * 10}' y='#{i * 10}' width='10' height='10' #{attrs}/>"
16
+ end
17
+
18
+ <<~SVG
19
+ <?xml version="1.0"?>
20
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
21
+ #{elements.join("\n ")}
22
+ </svg>
23
+ SVG
24
+ end
25
+
26
+ puts "=" * 80
27
+ puts "svg_conform Performance Benchmark"
28
+ puts "=" * 80
29
+ puts
30
+
31
+ # Test configurations
32
+ test_cases = [
33
+ { name: "Small document", elements: 10, attributes: 3 },
34
+ { name: "Medium document", elements: 50, attributes: 5 },
35
+ { name: "Large document", elements: 100, attributes: 8 },
36
+ ]
37
+
38
+ validator = SvgConform::Validator.new
39
+
40
+ test_cases.each do |test_case|
41
+ puts "Test: #{test_case[:name]}"
42
+ puts " Elements: #{test_case[:elements]}, Attributes per element: #{test_case[:attributes]}"
43
+ puts
44
+
45
+ svg_content = create_test_svg(element_count: test_case[:elements],
46
+ attribute_count: test_case[:attributes])
47
+
48
+ # Warmup run
49
+ validator.validate(svg_content, profile: :svg_1_2_rfc)
50
+
51
+ # Benchmark single validation
52
+ single_time = Benchmark.measure do
53
+ 100.times do
54
+ validator.validate(svg_content, profile: :svg_1_2_rfc)
55
+ end
56
+ end
57
+
58
+ puts " Single validation (100 runs): #{(single_time.real * 1000).round(2)}ms"
59
+ puts " Average per validation: #{(single_time.real * 10).round(2)}ms"
60
+
61
+ # Benchmark batch validation
62
+ contents = Array.new(10) do
63
+ create_test_svg(element_count: test_case[:elements],
64
+ attribute_count: test_case[:attributes])
65
+ end
66
+
67
+ batch_time = Benchmark.measure do
68
+ 10.times do
69
+ contents.each do |content|
70
+ validator.validate(content, profile: :svg_1_2_rfc)
71
+ end
72
+ end
73
+ end
74
+
75
+ puts " Batch validation (10 files × 10 runs): #{(batch_time.real * 1000).round(2)}ms"
76
+ puts " Average per file: #{(batch_time.real * 100).round(2)}ms"
77
+ puts
78
+ end
79
+
80
+ puts "=" * 80
81
+ puts "Memory Profiling"
82
+ puts "=" * 80
83
+ puts
84
+
85
+ if Object.const_defined?(:GC)
86
+ GC.start
87
+ before_mem = `ps -o rss= -p #{Process.pid}`.to_i
88
+
89
+ # Create and validate 100 documents
90
+ 100.times do
91
+ svg = create_test_svg(element_count: 50, attribute_count: 5)
92
+ validator.validate(svg, profile: :svg_1_2_rfc)
93
+ end
94
+
95
+ GC.start
96
+ after_mem = `ps -o rss= -p #{Process.pid}`.to_i
97
+
98
+ mem_increase = after_mem - before_mem
99
+ puts " RSS memory increase: #{mem_increase} KB"
100
+ puts " Per validation: #{(mem_increase / 100.0).round(2)} KB"
101
+ else
102
+ puts " Memory profiling not available on this platform"
103
+ end
104
+
105
+ puts
106
+ puts "=" * 80
107
+ puts "Benchmark Complete"
108
+ puts "=" * 80
109
+ puts
110
+ puts "Optimizations tested:"
111
+ puts " ✓ Phase 1.1: ElementProxy#path_id memoization"
112
+ puts " ✓ Phase 1.2: Element configuration index"
113
+ puts " ✓ Phase 2.1: Requirement classification cache"
114
+ puts " ✓ Phase 2.2: Global properties constant"
115
+ puts " ✓ Phase 2.3: ElementProxy attributes cache"
116
+ puts " ✓ Phase 3.1: Configuration validation cache"
117
+ puts " ✓ Phase 3.3: ProfileCompiler class"
118
+ puts " ✓ Phase 4.1: TrackerFactory extraction"
119
+ puts " ✓ Phase 5.1: Batch validation optimization"
120
+ puts
@@ -0,0 +1,239 @@
1
+ = Performance Optimization Guide
2
+
3
+ This guide documents the performance optimization patterns used in svg_conform.
4
+
5
+ == Goals
6
+
7
+ The performance optimization strategy focuses on:
8
+
9
+ * Reducing validation time for repeated operations
10
+ * Minimizing memory allocations during SAX parsing
11
+ * Caching expensive operations without introducing complexity
12
+ * Maintaining clean separation of concerns
13
+
14
+ == Optimization Patterns
15
+
16
+ === Memoization
17
+
18
+ Cache computed values to avoid repeated calculations.
19
+
20
+ *Example:* `ElementProxy#path_id` uses `@cached_path_id` to store the computed path:
21
+
22
+ [source,ruby]
23
+ ----
24
+ def path_id
25
+ @cached_path_id ||= begin
26
+ parts = @path + ["#{@name}[#{@position]}"]
27
+ "/#{parts.join('/')}"
28
+ end
29
+ end
30
+ ----
31
+
32
+ *When to use:* When a method is called multiple times with the same result and the computation is expensive.
33
+
34
+ *When to avoid:* When the result changes between calls, or the computation is trivial.
35
+
36
+ === Class-Level Caching with Thread Safety
37
+
38
+ Use class-level caches with Mutex for thread-safe shared state.
39
+
40
+ *Example:* Requirement classification cache in `SaxValidationHandler`:
41
+
42
+ [source,ruby]
43
+ ----
44
+ @classification_cache = {}
45
+ @classification_cache_mutex = Mutex.new
46
+
47
+ def classify_requirements_with_cache
48
+ profile_key = @profile.class.name
49
+ classified = self.class.classification_cache_mutex.synchronize do
50
+ self.class.classification_cache[profile_key]
51
+ end
52
+
53
+ # ... compute and cache if not found
54
+ end
55
+ ----
56
+
57
+ *When to use:* When the cached value is shared across all instances and doesn't change.
58
+
59
+ *When to avoid:* When the cached value is instance-specific or changes frequently.
60
+
61
+ === Frozen Constants
62
+
63
+ Define immutable data as frozen constants to avoid reallocations.
64
+
65
+ *Example:* `GLOBAL_PROPERTIES` constant in `AllowedElementsRequirement`:
66
+
67
+ [source,ruby]
68
+ ----
69
+ GLOBAL_PROPERTIES = %w[
70
+ about base baseprofile d break class content cx cy
71
+ # ... full list of properties
72
+ ].freeze
73
+ ----
74
+
75
+ *When to use:* For lookup tables, configuration data, and other immutable collections.
76
+
77
+ *When to avoid:* When the data needs to be modified at runtime.
78
+
79
+ === Hash Indexes for O(1) Lookups
80
+
81
+ Pre-build hash indexes for fast lookups instead of linear array searches.
82
+
83
+ *Example:* Element configuration index in `AllowedElementsRequirement`:
84
+
85
+ [source,ruby]
86
+ ----
87
+ def after_initialize
88
+ build_element_config_index if element_configs&.any?
89
+ end
90
+
91
+ def build_element_config_index
92
+ @element_config_index = {}
93
+ element_configs.each do |config|
94
+ @element_config_index[config.tag] = config
95
+ end
96
+ end
97
+
98
+ # Usage - O(1) instead of O(n)
99
+ element_config = @element_config_index&.dig(element_name)
100
+ ----
101
+
102
+ *When to use:* When frequently looking up items by key in a collection.
103
+
104
+ *When to avoid:* When the collection is small or accessed sequentially.
105
+
106
+ === Lazy Initialization with `||=`
107
+
108
+ Use the "or-equals" operator for lazy initialization.
109
+
110
+ *Example:* Cached attributes array in `ElementProxy`:
111
+
112
+ [source,ruby]
113
+ ----
114
+ def attributes
115
+ @cached_attributes ||= @raw_attributes.map { |name, value| SaxAttribute.new(name, value) }
116
+ end
117
+ ----
118
+
119
+ *When to use:* When a value is expensive to compute and may not be needed.
120
+
121
+ *When to avoid:* When `nil` or `false` are valid cached values (use explicit nil check instead).
122
+
123
+ === Batch Processing Optimization
124
+
125
+ Amortize fixed costs across multiple operations.
126
+
127
+ *Example:* Batch file validation in `Validator`:
128
+
129
+ [source,ruby]
130
+ ----
131
+ def validate_files(file_paths, profile: :svg_1_2_rfc, **options)
132
+ # Load profile once
133
+ profile_obj = resolve_profile(profile)
134
+
135
+ file_paths.each do |file_path|
136
+ validate_file_with_profile(file_path, profile_obj, **options)
137
+ end
138
+ end
139
+ ----
140
+
141
+ *When to use:* When processing multiple items with shared setup costs.
142
+
143
+ *When to avoid:* When processing single items or when items have different configurations.
144
+
145
+ === Factory Pattern for Complex Initialization
146
+
147
+ Centralize object creation for cleaner initialization.
148
+
149
+ *Example:* `TrackerFactory` for creating validation trackers:
150
+
151
+ [source,ruby]
152
+ ----
153
+ module Validation
154
+ module TrackerFactory
155
+ def self.create_all_trackers(document)
156
+ node_id_manager = create_node_id_manager(document)
157
+
158
+ {
159
+ error_tracker: create_error_tracker,
160
+ node_id_manager: node_id_manager,
161
+ # ... other trackers
162
+ }
163
+ end
164
+ end
165
+ end
166
+ ----
167
+
168
+ *When to use:* When initialization involves multiple related objects or complex logic.
169
+
170
+ *When to avoid:* For simple object creation that doesn't benefit from centralization.
171
+
172
+ == Anti-Patterns to Avoid
173
+
174
+ === Premature Optimization
175
+
176
+ Don't optimize without benchmarks. Measure first, then optimize.
177
+
178
+ === Global Mutable State
179
+
180
+ Avoid mutable global state. Use class-level caches with thread-safe access.
181
+
182
+ === Over-Abstraction
183
+
184
+ Don't create abstractions just for the sake of it. Each abstraction should pull its weight.
185
+
186
+ === Optimizing Uncommon Paths
187
+
188
+ Focus on hot paths. Don't optimize code that rarely runs.
189
+
190
+ === Sacrificing Readability
191
+
192
+ Performance gains should not come at the cost of code clarity.
193
+
194
+ == Performance Metrics
195
+
196
+ The `benchmark/performance_comparison.rb` script measures:
197
+
198
+ * Single validation speed (average time per document)
199
+ * Batch validation speed (average time per file)
200
+ * Memory usage per validation
201
+
202
+ Run benchmarks before and after optimizations to measure impact:
203
+
204
+ [source,shell]
205
+ ----
206
+ ruby benchmark/performance_comparison.rb
207
+ ----
208
+
209
+ == Architecture Considerations
210
+
211
+ === SAX vs DOM Validation
212
+
213
+ svg_conform uses SAX validation by default for better performance:
214
+
215
+ * *SAX mode*: Constant memory, streaming parser, handles large files
216
+ * *DOM mode*: Loads full document, uses more memory, slower for large files
217
+
218
+ Always prefer SAX for validation. Only use DOM when applying remediations.
219
+
220
+ === Caching Strategy
221
+
222
+ * *Instance-level cache*: Use for data specific to a validation run
223
+ * *Class-level cache*: Use for data shared across all instances
224
+ * *Thread safety*: Always use Mutex for class-level shared state
225
+
226
+ == Future Optimization Opportunities
227
+
228
+ Areas that may benefit from future optimization:
229
+
230
+ * Parallel batch validation for multi-core systems
231
+ * Compiled profiles with pre-computed lookup tables
232
+ * Streaming remediation to avoid full DOM load
233
+ * Attribute name interning for reduced memory
234
+
235
+ == Further Reading
236
+
237
+ * `PERFORMANCE_OPTIMIZATION_PLAN.md` - Detailed optimization plan
238
+ * `CLAUDE.md` - Architecture overview
239
+ * `benchmark/performance_comparison.rb` - Performance benchmarks
@@ -32,15 +32,20 @@ module SvgConform
32
32
  @child_counters = {} # Track child element positions
33
33
  end
34
34
 
35
- # Build full path ID for this element
35
+ # Build full path ID for this element (memoized for performance)
36
36
  def path_id
37
- parts = @path + ["#{@name}[#{@position}]"]
38
- "/#{parts.join('/')}"
37
+ @path_id ||= begin
38
+ parts = @path + ["#{@name}[#{@position}]"]
39
+ "/#{parts.join('/')}"
40
+ end
39
41
  end
40
42
 
41
- # Return attributes as array of SaxAttribute objects (for compatibility)
43
+ # Return attributes as array of SaxAttribute objects (memoized for performance)
44
+ # Cached to avoid repeated object allocations during SAX parsing
42
45
  def attributes
43
- @raw_attributes.map { |name, value| SaxAttribute.new(name, value) }
46
+ @attributes ||= @raw_attributes.map do |name, value|
47
+ SaxAttribute.new(name, value)
48
+ end
44
49
  end
45
50
 
46
51
  # Check if this element has a specific attribute
@@ -75,8 +80,8 @@ module SvgConform
75
80
  # Boolean check
76
81
  has_attribute?(method.to_s.chomp("?"))
77
82
  else
78
- # Attribute access
79
- @attributes[method.to_s] || @attributes[method.to_sym]
83
+ # Attribute access - use raw_attributes hash, not cached array
84
+ @raw_attributes[method.to_s] || @raw_attributes[method.to_sym]
80
85
  end
81
86
  end
82
87
 
@@ -7,6 +7,8 @@ require_relative "remediations"
7
7
  module SvgConform
8
8
  # Base class for SVG validation profiles using lutaml-model serialization
9
9
  class Profile < Lutaml::Model::Serializable
10
+ PROFILES_DIR = File.expand_path("../../config/profiles", __dir__)
11
+
10
12
  attribute :name, :string
11
13
  attribute :description, :string
12
14
  attribute :import, :string
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "profile"
4
+
5
+ module SvgConform
6
+ # Compiles and prepares SVG validation profiles
7
+ # Centralizes profile loading logic for better separation of concerns
8
+ class ProfileCompiler
9
+ @compile_cache = {}
10
+ @compile_mutex = Mutex.new
11
+
12
+ class << self
13
+ attr_reader :compile_cache, :compile_mutex
14
+
15
+ # Compile a profile by name
16
+ # Returns a cached profile if already compiled
17
+ def compile(profile_id)
18
+ profile_key = profile_id.to_s
19
+
20
+ # Check cache (thread-safe)
21
+ compile_mutex.synchronize do
22
+ return compile_cache[profile_key] if compile_cache[profile_key]
23
+ end
24
+
25
+ # Load and compile profile
26
+ profile = load_profile(profile_key)
27
+ compiled_profile = prepare_profile(profile)
28
+
29
+ # Cache result (thread-safe)
30
+ compile_mutex.synchronize do
31
+ compile_cache[profile_key] = compiled_profile
32
+ end
33
+
34
+ compiled_profile
35
+ end
36
+
37
+ # Compile from YAML content
38
+ def compile_from_yaml(yaml_content)
39
+ profile = Profile.from_yaml(yaml_content)
40
+ prepare_profile(profile)
41
+ end
42
+
43
+ # Compile from file path
44
+ def compile_from_file(file_path)
45
+ profile = Profile.load_from_file(file_path)
46
+ prepare_profile(profile)
47
+ end
48
+
49
+ # Clear compilation cache
50
+ def clear_cache!
51
+ compile_mutex.synchronize do
52
+ compile_cache.clear
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Load profile from profiles directory
59
+ def load_profile(profile_name)
60
+ profile_file = File.join(Profile::PROFILES_DIR, "#{profile_name}.yml")
61
+
62
+ unless File.exist?(profile_file)
63
+ raise ProfileError,
64
+ "Profile not found: #{profile_name} (expected: #{profile_file})"
65
+ end
66
+
67
+ Profile.load_from_file(profile_file)
68
+ end
69
+
70
+ # Prepare profile for use (validation and preprocessing)
71
+ def prepare_profile(profile)
72
+ # Validate profile has required fields
73
+ validate_profile(profile)
74
+
75
+ # Pre-compute requirement classifications for SAX validation
76
+ precompute_requirement_metadata(profile)
77
+
78
+ profile
79
+ end
80
+
81
+ # Validate profile structure
82
+ def validate_profile(profile)
83
+ if profile.name.nil? || profile.name.empty?
84
+ raise ProfileError,
85
+ "Profile must have a name"
86
+ end
87
+ end
88
+
89
+ # Pre-compute metadata for faster validation
90
+ def precompute_requirement_metadata(profile)
91
+ # Trigger requirement classification to populate caches
92
+ profile.requirements.each do |req|
93
+ # Access classification method to populate SaxValidationHandler cache
94
+ if req.respond_to?(:needs_deferred_validation?)
95
+ req.needs_deferred_validation?
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -4,7 +4,6 @@ require_relative "profile"
4
4
 
5
5
  module SvgConform
6
6
  module Profiles
7
- PROFILES_DIR = File.expand_path("../../config/profiles", __dir__)
8
7
  @@cache = {}
9
8
 
10
9
  def self.get(profile_id)
@@ -21,7 +20,7 @@ module SvgConform
21
20
  def self.available_profiles
22
21
  return @@cache.keys.map(&:to_sym) if @@cache.any?
23
22
 
24
- profile_files = Dir.glob(File.join(PROFILES_DIR, "*.yml"))
23
+ profile_files = Dir.glob(File.join(Profile::PROFILES_DIR, "*.yml"))
25
24
  profile_files.map { |file| File.basename(file, ".yml").to_sym }
26
25
  end
27
26
 
@@ -40,7 +39,7 @@ module SvgConform
40
39
  def self.load_profile(profile_name)
41
40
  return @@cache[profile_name] if @@cache[profile_name]
42
41
 
43
- profile_file = File.join(PROFILES_DIR, "#{profile_name}.yml")
42
+ profile_file = File.join(Profile::PROFILES_DIR, "#{profile_name}.yml")
44
43
 
45
44
  unless File.exist?(profile_file)
46
45
  raise ProfileError,
@@ -32,6 +32,32 @@ module SvgConform
32
32
  "http://www.w3.org/2000/01/rdf-schema#",
33
33
  ].freeze
34
34
 
35
+ # Global properties allowed on any element (from svgcheck word_properties.py)
36
+ # Defined once at class level to avoid repeated array allocations
37
+ GLOBAL_PROPERTIES = %w[
38
+ about base baseprofile d break class content cx cy datatype height href
39
+ label lang pathlength points preserveaspectratio property r rel resource
40
+ rev role rotate rx ry space snapshottime transform typeof version width
41
+ viewbox x x1 x2 y y1 y2 stroke stroke-width stroke-linecap stroke-linejoin
42
+ stroke-miterlimit stroke-dasharray stroke-dashoffset stroke-opacity
43
+ vector-effect viewport-fill display viewport-fill-opacity visibility
44
+ image-rendering color-rendering shape-rendering text-rendering
45
+ buffered-rendering solid-opacity solid-color color stop-color stop-opacity
46
+ line-increment text-align display-align font-size font-family font-weight
47
+ font-style font-variant direction unicode-bidi text-anchor fill fill-rule
48
+ fill-opacity requiredfeatures requiredformats requiredextensions
49
+ requiredfonts systemlanguage
50
+ ].freeze
51
+
52
+ # Class-level cache for validated configurations
53
+ @configuration_validation_cache = {}
54
+ @configuration_validation_mutex = Mutex.new
55
+
56
+ class << self
57
+ attr_reader :configuration_validation_cache,
58
+ :configuration_validation_mutex
59
+ end
60
+
35
61
  yaml do
36
62
  map "id", to: :id
37
63
  map "description", to: :description
@@ -47,10 +73,48 @@ module SvgConform
47
73
  map "allow_rdf_metadata", to: :allow_rdf_metadata
48
74
  end
49
75
 
76
+ def after_initialize
77
+ build_element_config_index if element_configs&.any?
78
+ end
79
+
80
+ # Ensure element config index is built (lazy initialization)
81
+ def element_config_index
82
+ @element_config_index ||= build_element_config_index
83
+ end
84
+
85
+ # Build element configuration index for O(1) lookup
86
+ def build_element_config_index
87
+ return {} unless element_configs&.any?
88
+
89
+ index = {}
90
+ element_configs.each do |config|
91
+ index[config.tag] = config
92
+ index["*"] = config if config.tag == "*"
93
+ end
94
+ index
95
+ end
96
+
50
97
  # Check for configuration conflicts and emit warnings
98
+ # Uses class-level cache to skip validation for identical configurations
51
99
  def validate_configuration
52
100
  return if allowed_attribute_patterns.empty? || !element_configs&.any?
53
101
 
102
+ # Create cache key from configuration
103
+ config_key = {
104
+ patterns: allowed_attribute_patterns.sort,
105
+ element_configs: element_configs.map do |ec|
106
+ { tag: ec.tag, attr: ec.attr&.sort }
107
+ end,
108
+ }.hash
109
+
110
+ # Check cache (thread-safe)
111
+ already_validated = self.class.configuration_validation_mutex.synchronize do
112
+ self.class.configuration_validation_cache[config_key]
113
+ end
114
+
115
+ return if already_validated
116
+
117
+ # Perform validation
54
118
  element_configs.each do |element_config|
55
119
  next unless element_config&.attr
56
120
 
@@ -75,15 +139,16 @@ module SvgConform
75
139
  "Allowed patterns take precedence over element-specific disallowed attributes."
76
140
  end
77
141
  end
142
+
143
+ # Mark as validated (thread-safe)
144
+ self.class.configuration_validation_mutex.synchronize do
145
+ self.class.configuration_validation_cache[config_key] = true
146
+ end
78
147
  end
79
148
 
80
149
  def check(node, context)
81
- # Validate configuration once on first use
82
- @_config_validated ||= false
83
- unless @_config_validated
84
- validate_configuration
85
- @_config_validated = true
86
- end
150
+ # Validate configuration (uses class-level cache to skip redundant validations)
151
+ validate_configuration
87
152
 
88
153
  return unless element?(node)
89
154
 
@@ -158,12 +223,8 @@ module SvgConform
158
223
  end
159
224
 
160
225
  def validate_sax_element(element, context)
161
- # Validate configuration once on first use
162
- @_config_validated ||= false
163
- unless @_config_validated
164
- validate_configuration
165
- @_config_validated = true
166
- end
226
+ # Validate configuration (uses class-level cache to skip redundant validations)
227
+ validate_configuration
167
228
 
168
229
  # Skip if parent is structurally invalid (matches DOM behavior)
169
230
  if element.parent && context.node_structurally_invalid?(element.parent)
@@ -261,10 +322,8 @@ module SvgConform
261
322
  def invalid_parent_child?(parent_name, child_name)
262
323
  return false unless element_configs&.any?
263
324
 
264
- # Find the configuration for the parent element
265
- parent_config = element_configs.find do |config|
266
- config.tag == parent_name
267
- end
325
+ # Find the configuration for the parent element (O(1) with index)
326
+ parent_config = element_config_index&.dig(parent_name)
268
327
  return false unless parent_config
269
328
 
270
329
  # If allowed_children is defined and not empty, use it
@@ -296,9 +355,7 @@ module SvgConform
296
355
 
297
356
  return errors unless element_configs&.any?
298
357
 
299
- element_config = element_configs.find do |config|
300
- config.tag == element_name
301
- end
358
+ element_config = element_config_index&.dig(element_name)
302
359
 
303
360
  return errors unless element_config&.attr
304
361
 
@@ -319,21 +376,8 @@ module SvgConform
319
376
  allowed_attrs = (allowed_attrs + common_attrs).uniq
320
377
 
321
378
  # Add global properties that svgcheck allows on any element (from word_properties.py)
322
- global_properties = %w[
323
- about base baseprofile d break class content cx cy datatype height href
324
- label lang pathlength points preserveaspectratio property r rel resource
325
- rev role rotate rx ry space snapshottime transform typeof version width
326
- viewbox x x1 x2 y y1 y2 stroke stroke-width stroke-linecap stroke-linejoin
327
- stroke-miterlimit stroke-dasharray stroke-dashoffset stroke-opacity
328
- vector-effect viewport-fill display viewport-fill-opacity visibility
329
- image-rendering color-rendering shape-rendering text-rendering
330
- buffered-rendering solid-opacity solid-color color stop-color stop-opacity
331
- line-increment text-align display-align font-size font-family font-weight
332
- font-style font-variant direction unicode-bidi text-anchor fill fill-rule
333
- fill-opacity requiredfeatures requiredformats requiredextensions
334
- requiredfonts systemlanguage
335
- ]
336
- allowed_attrs = (allowed_attrs + global_properties).uniq
379
+ # GLOBAL_PROPERTIES is defined at class level to avoid repeated allocations
380
+ allowed_attrs = (allowed_attrs + GLOBAL_PROPERTIES).uniq
337
381
 
338
382
  node.attributes.each do |attr|
339
383
  attr_name = attr.name.downcase
@@ -378,7 +422,7 @@ module SvgConform
378
422
  # Check for globally disallowed attributes (using * tag)
379
423
  return errors unless element_configs&.any?
380
424
 
381
- global_config = element_configs.find { |config| config.tag == "*" }
425
+ global_config = element_config_index&.dig("*")
382
426
  return errors unless global_config&.attr
383
427
 
384
428
  global_disallowed = []
@@ -446,9 +490,7 @@ module SvgConform
446
490
 
447
491
  return errors unless element_configs&.any?
448
492
 
449
- element_config = element_configs.find do |config|
450
- config.tag == element_name
451
- end
493
+ element_config = element_config_index&.dig(element_name)
452
494
  return errors unless element_config&.attr
453
495
 
454
496
  allowed_attrs = []
@@ -467,22 +509,9 @@ module SvgConform
467
509
  common_attrs = %w[id class style xmlns]
468
510
  allowed_attrs = (allowed_attrs + common_attrs).uniq
469
511
 
470
- # Add global properties
471
- global_properties = %w[
472
- about base baseprofile d break class content cx cy datatype height href
473
- label lang pathlength points preserveaspectratio property r rel resource
474
- rev role rotate rx ry space snapshottime transform typeof version width
475
- viewbox x x1 x2 y y1 y2 stroke stroke-width stroke-linecap stroke-linejoin
476
- stroke-miterlimit stroke-dasharray stroke-dashoffset stroke-opacity
477
- vector-effect viewport-fill display viewport-fill-opacity visibility
478
- image-rendering color-rendering shape-rendering text-rendering
479
- buffered-rendering solid-opacity solid-color color stop-color stop-opacity
480
- line-increment text-align display-align font-size font-family font-weight
481
- font-style font-variant direction unicode-bidi text-anchor fill fill-rule
482
- fill-opacity requiredfeatures requiredformats requiredextensions
483
- requiredfonts systemlanguage
484
- ]
485
- allowed_attrs = (allowed_attrs + global_properties).uniq
512
+ # Add global properties that svgcheck allows on any element (from word_properties.py)
513
+ # GLOBAL_PROPERTIES is defined at class level to avoid repeated allocations
514
+ allowed_attrs = (allowed_attrs + GLOBAL_PROPERTIES).uniq
486
515
 
487
516
  element.attributes.each do |attr|
488
517
  attr_name = attr.name.downcase
@@ -527,7 +556,7 @@ module SvgConform
527
556
 
528
557
  return errors unless element_configs&.any?
529
558
 
530
- global_config = element_configs.find { |config| config.tag == "*" }
559
+ global_config = element_config_index&.dig("*")
531
560
  return errors unless global_config&.attr
532
561
 
533
562
  global_disallowed = []
@@ -15,6 +15,14 @@ module SvgConform
15
15
  class SaxValidationHandler < Nokogiri::XML::SAX::Document
16
16
  attr_reader :result, :context
17
17
 
18
+ # Class-level cache for requirement classifications by profile class
19
+ @classification_cache = {}
20
+ @classification_cache_mutex = Mutex.new
21
+
22
+ class << self
23
+ attr_reader :classification_cache, :classification_cache_mutex
24
+ end
25
+
18
26
  def initialize(profile)
19
27
  @profile = profile
20
28
  @element_stack = [] # Track parent-child hierarchy
@@ -26,10 +34,8 @@ module SvgConform
26
34
  # Create validation context (without document reference for SAX)
27
35
  @context = create_sax_context
28
36
 
29
- # Classify requirements into immediate vs deferred
30
- @immediate_requirements = []
31
- @deferred_requirements = []
32
- classify_requirements
37
+ # Classify requirements into immediate vs deferred (use cache if available)
38
+ @immediate_requirements, @deferred_requirements = classify_requirements_with_cache
33
39
  end
34
40
 
35
41
  # SAX Event: Document start
@@ -138,28 +144,77 @@ module SvgConform
138
144
 
139
145
  # Create a SAX-compatible validation context
140
146
  def create_sax_context
141
- # Create context without triggering DOM operations
147
+ # Create context using TrackerFactory for cleaner initialization
148
+ trackers = Validation::TrackerFactory.create_all_trackers(nil)
149
+
142
150
  context = ValidationContext.allocate
143
151
  context.instance_variable_set(:@document, nil)
144
152
  context.instance_variable_set(:@profile, @profile)
145
- context.instance_variable_set(:@error_tracker, Validation::ErrorTracker.new)
146
- context.instance_variable_set(:@fixes, [])
147
- context.instance_variable_set(:@data, {})
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
153
+ context.instance_variable_set(:@error_tracker, trackers[:error_tracker])
154
+ context.instance_variable_set(:@node_id_manager,
155
+ trackers[:node_id_manager])
152
156
  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
- ))
157
+ trackers[:structural_invalidity_tracker])
158
158
  context.instance_variable_set(:@reference_manifest,
159
- References::ReferenceManifest.new(source_document: nil))
159
+ trackers[:reference_manifest])
160
+ context.instance_variable_set(:@fixes, [])
161
+ context.instance_variable_set(:@data, {})
160
162
  context
161
163
  end
162
164
 
165
+ # Classify requirements based on validation needs (with caching)
166
+ def classify_requirements_with_cache
167
+ profile_key = @profile.class.name
168
+ profile_requirements = @profile.requirements
169
+
170
+ # Check cache first (thread-safe)
171
+ @immediate_requirements = []
172
+ @deferred_requirements = []
173
+
174
+ classified = self.class.classification_cache_mutex.synchronize do
175
+ self.class.classification_cache[profile_key]
176
+ end
177
+
178
+ if classified
179
+ @immediate_requirements = classified[:immediate].map do |req_class|
180
+ # Find the actual instance from profile requirements
181
+ profile_requirements.find { |r| r.is_a?(req_class) }
182
+ end.compact
183
+ @deferred_requirements = classified[:deferred].map do |req_class|
184
+ profile_requirements.find { |r| r.is_a?(req_class) }
185
+ end.compact
186
+ else
187
+ # Classify and cache
188
+ immediate_classes = []
189
+ deferred_classes = []
190
+
191
+ profile_requirements.each do |req|
192
+ if req.respond_to?(:needs_deferred_validation?) && req.needs_deferred_validation?
193
+ deferred_classes << req.class
194
+ else
195
+ immediate_classes << req.class
196
+ end
197
+ end
198
+
199
+ # Store in cache
200
+ self.class.classification_cache_mutex.synchronize do
201
+ self.class.classification_cache[profile_key] = {
202
+ immediate: immediate_classes,
203
+ deferred: deferred_classes,
204
+ }
205
+ end
206
+
207
+ @immediate_requirements = profile_requirements.select do |req|
208
+ immediate_classes.include?(req.class)
209
+ end
210
+ @deferred_requirements = profile_requirements.select do |req|
211
+ deferred_classes.include?(req.class)
212
+ end
213
+ end
214
+
215
+ [@immediate_requirements, @deferred_requirements]
216
+ end
217
+
163
218
  # Classify requirements based on validation needs
164
219
  def classify_requirements
165
220
  return unless @profile&.requirements
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../references"
4
+ require_relative "error_tracker"
5
+ require_relative "structural_invalidity_tracker"
6
+ require_relative "node_id_manager"
7
+
8
+ module SvgConform
9
+ module Validation
10
+ # Factory for creating tracker objects used in ValidationContext
11
+ # Centralizes tracker object creation for cleaner initialization
12
+ module TrackerFactory
13
+ # Create a new ErrorTracker instance
14
+ def self.create_error_tracker
15
+ ErrorTracker.new
16
+ end
17
+
18
+ # Create a new NodeIdManager instance
19
+ # @param document [Document, nil] The document (nil for SAX mode)
20
+ def self.create_node_id_manager(document)
21
+ NodeIdManager.new(document)
22
+ end
23
+
24
+ # Create a new StructuralInvalidityTracker instance
25
+ # @param node_id_generator [Proc] Callable that generates node IDs
26
+ def self.create_structural_invalidity_tracker(node_id_generator:)
27
+ StructuralInvalidityTracker.new(node_id_generator: node_id_generator)
28
+ end
29
+
30
+ # Create a new ReferenceManifest instance
31
+ # @param source_document [String, nil] Optional source document path
32
+ def self.create_reference_manifest(source_document: nil)
33
+ References::ReferenceManifest.new(source_document: source_document)
34
+ end
35
+
36
+ # Create all trackers for a ValidationContext
37
+ # @param document [Document, nil] The document (nil for SAX mode)
38
+ # @return [Hash] Hash of tracker instances
39
+ def self.create_all_trackers(document)
40
+ node_id_manager = create_node_id_manager(document)
41
+
42
+ {
43
+ error_tracker: create_error_tracker,
44
+ node_id_manager: node_id_manager,
45
+ structural_invalidity_tracker: create_structural_invalidity_tracker(
46
+ node_id_generator: ->(node) {
47
+ node_id_manager.generate_node_id(node)
48
+ },
49
+ ),
50
+ reference_manifest: create_reference_manifest(source_document: document&.file_path),
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -7,6 +7,7 @@ require_relative "errors/validation_notice"
7
7
  require_relative "validation/error_tracker"
8
8
  require_relative "validation/structural_invalidity_tracker"
9
9
  require_relative "validation/node_id_manager"
10
+ require_relative "validation/tracker_factory"
10
11
 
11
12
  module SvgConform
12
13
  # Context object passed to requirements during validation
@@ -91,16 +92,16 @@ module SvgConform
91
92
  def initialize(document, profile)
92
93
  @document = document
93
94
  @profile = profile
94
- @error_tracker = Validation::ErrorTracker.new
95
+
96
+ # Use TrackerFactory to create all tracker instances
97
+ trackers = Validation::TrackerFactory.create_all_trackers(document)
98
+ @error_tracker = trackers[:error_tracker]
99
+ @node_id_manager = trackers[:node_id_manager]
100
+ @structural_invalidity_tracker = trackers[:structural_invalidity_tracker]
101
+ @reference_manifest = trackers[:reference_manifest]
102
+
95
103
  @fixes = []
96
104
  @data = {}
97
- @node_id_manager = Validation::NodeIdManager.new(document)
98
- @structural_invalidity_tracker = Validation::StructuralInvalidityTracker.new(
99
- node_id_generator: ->(node) { @node_id_manager.generate_node_id(node) },
100
- )
101
- @reference_manifest = References::ReferenceManifest.new(
102
- source_document: document&.file_path,
103
- )
104
105
  end
105
106
 
106
107
  # Mark a node as structurally invalid (e.g., invalid parent-child relationship)
@@ -64,13 +64,19 @@ module SvgConform
64
64
  result
65
65
  end
66
66
 
67
- # Validate multiple files
67
+ # Validate multiple files efficiently
68
+ # Loads profile once and reuses across all validations for better performance
68
69
  def validate_files(file_paths, profile: :svg_1_2_rfc, **options)
70
+ merged_options = @options.merge(options)
71
+
72
+ # Load profile once to amortize cost across all validations
73
+ profile_obj = resolve_profile(profile)
74
+
69
75
  results = {}
70
76
 
71
77
  file_paths.each do |file_path|
72
78
  results[file_path] =
73
- validate_file(file_path, profile: profile, **options)
79
+ validate_file_with_profile(file_path, profile_obj, **merged_options)
74
80
  rescue StandardError => e
75
81
  results[file_path] = create_error_result(file_path, e)
76
82
  end
@@ -144,27 +150,42 @@ module SvgConform
144
150
 
145
151
  def validate_file_sax(file_path, profile:, **options)
146
152
  profile_obj = resolve_profile(profile)
147
- sax_doc = SvgConform::SaxDocument.from_file(file_path)
148
- result = sax_doc.validate_with_profile(profile_obj)
153
+ validate_file_with_profile(file_path, profile_obj, **options)
154
+ end
149
155
 
150
- # If fixing is requested, load DOM and apply remediations directly
151
- # Skip re-validation to avoid DOM performance penalty
152
- if options[:fix] && result.has_errors?
153
- dom_doc = SvgConform::Document.from_file(file_path)
156
+ # Validate file using pre-loaded profile object
157
+ # Used internally by validate_files for efficient batch processing
158
+ def validate_file_with_profile(file_path, profile_obj, **options)
159
+ unless File.exist?(file_path)
160
+ raise ValidationError,
161
+ "File not found: #{file_path}"
162
+ end
154
163
 
155
- # Apply remediations directly without re-validating
156
- changes = profile_obj.apply_remediations(dom_doc)
164
+ mode = determine_mode(file_path, options[:mode])
157
165
 
158
- # Write fixed output if specified
159
- if options[:fix_output] && changes.any?
160
- File.write(options[:fix_output], dom_doc.to_xml)
166
+ case mode
167
+ when :sax
168
+ sax_doc = SvgConform::SaxDocument.from_file(file_path)
169
+ result = sax_doc.validate_with_profile(profile_obj)
170
+
171
+ # If fixing is requested, load DOM and apply remediations directly
172
+ if options[:fix] && result.has_errors?
173
+ dom_doc = SvgConform::Document.from_file(file_path)
174
+ changes = profile_obj.apply_remediations(dom_doc)
175
+
176
+ # Write fixed output if specified
177
+ if options[:fix_output] && changes.any?
178
+ File.write(options[:fix_output], dom_doc.to_xml)
179
+ end
161
180
  end
162
181
 
163
- # Return original SAX validation result (errors already detected)
164
- # Note: We don't re-validate to avoid DOM performance cost
182
+ result
183
+ when :dom
184
+ document = SvgConform::Document.from_file(file_path)
185
+ profile_obj.validate(document).tap do |result|
186
+ result.apply_fixes if options[:fix] && result.fixable?
187
+ end
165
188
  end
166
-
167
- result
168
189
  end
169
190
 
170
191
  def validate_file_dom(file_path, profile:, **options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SvgConform
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.10"
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -16,4 +16,27 @@ RSpec.configure do |config|
16
16
  config.expect_with :rspec do |c|
17
17
  c.syntax = :expect
18
18
  end
19
+
20
+ # Clear class-level caches before each test to prevent pollution
21
+ config.before do
22
+ # Clear AllowedElementsRequirement configuration cache
23
+ if defined?(SvgConform::Requirements::AllowedElementsRequirement)
24
+ SvgConform::Requirements::AllowedElementsRequirement.configuration_validation_cache.clear
25
+ end
26
+
27
+ # Clear SaxValidationHandler classification cache
28
+ if defined?(SvgConform::SaxValidationHandler)
29
+ SvgConform::SaxValidationHandler.classification_cache.clear
30
+ end
31
+
32
+ # Clear ProfileCompiler compile cache
33
+ if defined?(SvgConform::ProfileCompiler)
34
+ SvgConform::ProfileCompiler.compile_cache.clear
35
+ end
36
+
37
+ # Clear Profiles module cache
38
+ if defined?(SvgConform::Profiles)
39
+ SvgConform::Profiles.clear_cache!
40
+ end
41
+ end
19
42
  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.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-22 00:00:00.000000000 Z
11
+ date: 2026-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -91,6 +91,7 @@ files:
91
91
  - Gemfile
92
92
  - README.adoc
93
93
  - Rakefile
94
+ - benchmark/performance_comparison.rb
94
95
  - config/profiles/base.yml
95
96
  - config/profiles/lucid_fix.yml
96
97
  - config/profiles/metanorma.yml
@@ -100,6 +101,7 @@ files:
100
101
  - config/svgcheck_mapping.yml
101
102
  - docs/api_reference.adoc
102
103
  - docs/cli_guide.adoc
104
+ - docs/performance.adoc
103
105
  - docs/profiles.adoc
104
106
  - docs/rdf_metadata_support.adoc
105
107
  - docs/reference_manifest.adoc
@@ -152,6 +154,7 @@ files:
152
154
  - lib/svg_conform/node_helpers.rb
153
155
  - lib/svg_conform/node_index_builder.rb
154
156
  - lib/svg_conform/profile.rb
157
+ - lib/svg_conform/profile_compiler.rb
155
158
  - lib/svg_conform/profiles.rb
156
159
  - lib/svg_conform/references.rb
157
160
  - lib/svg_conform/references/base_reference.rb
@@ -199,6 +202,7 @@ files:
199
202
  - lib/svg_conform/validation/error_tracker.rb
200
203
  - lib/svg_conform/validation/node_id_manager.rb
201
204
  - lib/svg_conform/validation/structural_invalidity_tracker.rb
205
+ - lib/svg_conform/validation/tracker_factory.rb
202
206
  - lib/svg_conform/validation_context.rb
203
207
  - lib/svg_conform/validation_issue.rb
204
208
  - lib/svg_conform/validation_result.rb