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 +4 -4
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +15 -14
- data/benchmark/performance_comparison.rb +120 -0
- data/docs/performance.adoc +239 -0
- data/lib/svg_conform/element_proxy.rb +12 -7
- data/lib/svg_conform/profile.rb +2 -0
- data/lib/svg_conform/profile_compiler.rb +101 -0
- data/lib/svg_conform/profiles.rb +2 -3
- data/lib/svg_conform/requirements/allowed_elements_requirement.rb +84 -55
- data/lib/svg_conform/sax_validation_handler.rb +73 -18
- data/lib/svg_conform/validation/tracker_factory.rb +55 -0
- data/lib/svg_conform/validation_context.rb +9 -8
- data/lib/svg_conform/validator.rb +38 -17
- data/lib/svg_conform/version.rb +1 -1
- data/spec/spec_helper.rb +23 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f59b89abd58959b19078c28ce450f6ee102a007176d102031a04233f924465e
|
|
4
|
+
data.tar.gz: 61e68396baf1673fcd8d764a2d141cd62006d1167d2cea0b9d8695a7bb7ea6c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffb22ae1a52cbf687b054dbe17ccf8aa44b17bbeeaad5122b57300e9562e73e9cbf005d0c330b2ce13c28a7e6f349ddd191b3d8e8918c6e8745d663debcbec7c
|
|
7
|
+
data.tar.gz: dedcb6619d41fc142b0bb6432f15a693b38699ca52b80ba0e255d38a7feca6a86fca6a52daa15f5e694b8f4ca1da323cf67289129305b99a3f15616df336415a
|
data/.gitignore
CHANGED
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-
|
|
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:
|
|
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:
|
|
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:
|
|
84
|
+
# Offense count: 2
|
|
85
85
|
# Configuration parameters: CountBlocks, CountModifierForms.
|
|
86
86
|
Metrics/BlockNesting:
|
|
87
87
|
Max: 4
|
|
88
88
|
|
|
89
|
-
# Offense count:
|
|
89
|
+
# Offense count: 125
|
|
90
90
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
91
91
|
Metrics/CyclomaticComplexity:
|
|
92
92
|
Enabled: false
|
|
93
93
|
|
|
94
|
-
# Offense count:
|
|
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:
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
-
@
|
|
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
|
|
data/lib/svg_conform/profile.rb
CHANGED
|
@@ -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
|
data/lib/svg_conform/profiles.rb
CHANGED
|
@@ -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
|
|
82
|
-
|
|
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
|
|
162
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
323
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
472
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
146
|
-
context.instance_variable_set(:@
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
153
|
+
validate_file_with_profile(file_path, profile_obj, **options)
|
|
154
|
+
end
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
changes = profile_obj.apply_remediations(dom_doc)
|
|
164
|
+
mode = determine_mode(file_path, options[:mode])
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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)
|
data/lib/svg_conform/version.rb
CHANGED
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.
|
|
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-
|
|
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
|