svg_conform 0.1.8 → 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: 28ff5f6535023bd5817354101e080198c1f5e8b6a9cf0694555a45734e653283
4
- data.tar.gz: ef1f87272855005ac53e4bc2f06467298c3e58dd3a332a5207d9f27aabf3b9a4
3
+ metadata.gz: 8f59b89abd58959b19078c28ce450f6ee102a007176d102031a04233f924465e
4
+ data.tar.gz: 61e68396baf1673fcd8d764a2d141cd62006d1167d2cea0b9d8695a7bb7ea6c2
5
5
  SHA512:
6
- metadata.gz: d4a56e3f70abddf80258a6774300c99db15d996f892ba8c8fd6a7d9e78d7c19432a3264d8958312c5e7cff5006628a648775dd3cbc4d06d9de0fde5ef179e9d3
7
- data.tar.gz: 93d8c6ef9a74d628fa6b48aa4ab92ba41c4abc804d29d8b2670656e6114e869b614c0fef80363d393e7d72311e903a113fcf74cb060e8d41ea54f528a0442a77
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,80 +1,26 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-01-21 09:23:23 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
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 4
10
- # This cop supports safe autocorrection (--autocorrect).
11
- # Configuration parameters: EnforcedStyle, IndentationWidth.
12
- # SupportedStyles: with_first_argument, with_fixed_indentation
13
- Layout/ArgumentAlignment:
14
- Exclude:
15
- - 'lib/svg_conform/validation_context.rb'
16
-
17
- # Offense count: 2
9
+ # Offense count: 1
18
10
  # This cop supports safe autocorrection (--autocorrect).
19
11
  # Configuration parameters: EnforcedStyleAlignWith.
20
12
  # SupportedStylesAlignWith: either, start_of_block, start_of_line
21
13
  Layout/BlockAlignment:
22
14
  Exclude:
23
- - 'lib/svg_conform/sax_validation_handler.rb'
24
15
  - 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
25
16
 
26
- # Offense count: 1
27
- # This cop supports safe autocorrection (--autocorrect).
28
- Layout/BlockEndNewline:
29
- Exclude:
30
- - 'lib/svg_conform/sax_validation_handler.rb'
31
-
32
- # Offense count: 1
33
- # This cop supports safe autocorrection (--autocorrect).
34
- Layout/ElseAlignment:
35
- Exclude:
36
- - 'lib/svg_conform/requirements/no_external_css_requirement.rb'
37
-
38
- # Offense count: 1
39
- # This cop supports safe autocorrection (--autocorrect).
40
- # Configuration parameters: EnforcedStyleAlignWith.
41
- # SupportedStylesAlignWith: keyword, variable, start_of_line
42
- Layout/EndAlignment:
43
- Exclude:
44
- - 'lib/svg_conform/requirements/no_external_css_requirement.rb'
45
-
46
- # Offense count: 4
47
- # This cop supports safe autocorrection (--autocorrect).
48
- # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
49
- # SupportedHashRocketStyles: key, separator, table
50
- # SupportedColonStyles: key, separator, table
51
- # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
52
- Layout/HashAlignment:
53
- Exclude:
54
- - 'lib/svg_conform/validation_context.rb'
55
-
56
- # Offense count: 4
57
- # This cop supports safe autocorrection (--autocorrect).
58
- # Configuration parameters: Width, AllowedPatterns.
59
- Layout/IndentationWidth:
60
- Exclude:
61
- - 'lib/svg_conform/requirements/no_external_css_requirement.rb'
62
- - 'lib/svg_conform/sax_validation_handler.rb'
63
-
64
- # Offense count: 643
17
+ # Offense count: 620
65
18
  # This cop supports safe autocorrection (--autocorrect).
66
19
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
67
20
  # URISchemes: http, https
68
21
  Layout/LineLength:
69
22
  Enabled: false
70
23
 
71
- # Offense count: 2
72
- # This cop supports safe autocorrection (--autocorrect).
73
- # Configuration parameters: AllowInHeredoc.
74
- Layout/TrailingWhitespace:
75
- Exclude:
76
- - 'lib/svg_conform/validation_context.rb'
77
-
78
24
  # Offense count: 2
79
25
  # Configuration parameters: AllowedMethods.
80
26
  # AllowedMethods: enums
@@ -124,28 +70,28 @@ Lint/UnreachableCode:
124
70
  Exclude:
125
71
  - 'lib/svg_conform/commands/check.rb'
126
72
 
127
- # Offense count: 150
73
+ # Offense count: 146
128
74
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
129
75
  Metrics/AbcSize:
130
76
  Enabled: false
131
77
 
132
- # Offense count: 23
78
+ # Offense count: 24
133
79
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
134
80
  # AllowedMethods: refine
135
81
  Metrics/BlockLength:
136
82
  Max: 253
137
83
 
138
- # Offense count: 6
84
+ # Offense count: 2
139
85
  # Configuration parameters: CountBlocks, CountModifierForms.
140
86
  Metrics/BlockNesting:
141
87
  Max: 4
142
88
 
143
- # Offense count: 126
89
+ # Offense count: 125
144
90
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
145
91
  Metrics/CyclomaticComplexity:
146
92
  Enabled: false
147
93
 
148
- # Offense count: 262
94
+ # Offense count: 258
149
95
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
150
96
  Metrics/MethodLength:
151
97
  Max: 154
@@ -155,18 +101,11 @@ Metrics/MethodLength:
155
101
  Metrics/ParameterLists:
156
102
  Max: 9
157
103
 
158
- # Offense count: 100
104
+ # Offense count: 99
159
105
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
160
106
  Metrics/PerceivedComplexity:
161
107
  Enabled: false
162
108
 
163
- # Offense count: 4
164
- # Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns.
165
- # SupportedStyles: snake_case, camelCase
166
- Naming/VariableName:
167
- Exclude:
168
- - 'check_clippath_font.rb'
169
-
170
109
  # Offense count: 2
171
110
  # Configuration parameters: MinSize.
172
111
  Performance/CollectionLiteralInLoop:
@@ -193,11 +132,19 @@ RSpec/DescribeClass:
193
132
  - 'spec/svg_conform/references/integration_spec.rb'
194
133
  - 'spec/svgcheck_compatibility_spec.rb'
195
134
 
196
- # Offense count: 136
135
+ # Offense count: 139
197
136
  # Configuration parameters: CountAsOne.
198
137
  RSpec/ExampleLength:
199
138
  Max: 53
200
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
+
201
148
  # Offense count: 2
202
149
  RSpec/LeakyConstantDeclaration:
203
150
  Exclude:
@@ -258,11 +205,3 @@ Style/OptionalBooleanParameter:
258
205
  Style/RedundantCondition:
259
206
  Exclude:
260
207
  - 'lib/svg_conform/external_checkers/svgcheck/parser.rb'
261
-
262
- # Offense count: 1
263
- # This cop supports safe autocorrection (--autocorrect).
264
- # Configuration parameters: EnforcedStyleForMultiline.
265
- # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
266
- Style/TrailingCommaInArguments:
267
- Exclude:
268
- - 'lib/svg_conform/sax_validation_handler.rb'
@@ -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
@@ -16,6 +16,8 @@ requirements:
16
16
  allowed_namespaces:
17
17
  - "http://www.w3.org/2000/svg"
18
18
  - "" # Default namespace (no prefix)
19
+ allowed_attribute_patterns:
20
+ - "on*" # Allow event handler attributes (onmouseover, onmouseout, etc.) per issue #57
19
21
  element_configs:
20
22
  # Direct encoding from svgcheck word_properties.py elements dictionary
21
23
  - tag: "svg"
@@ -156,6 +158,7 @@ requirements:
156
158
  description: "Validates ID references point to existing elements"
157
159
 
158
160
  # Forbidden content - no multimedia, scripting, etc.
161
+ # Note: onmouseover, onmouseout, onfocus, onblur are intentionally allowed per issue #57
159
162
  - id: "forbidden_content"
160
163
  type: "ForbiddenContentRequirement"
161
164
  description: "Prohibits multimedia, scripting content"
@@ -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
data/docs/profiles.adoc CHANGED
@@ -268,10 +268,24 @@ See link:remediation.adoc#namespace-attribute-remediation[NamespaceAttributeReme
268
268
  * **Flexible colors**: Any colors allowed (unlike RFC 7996 black/white restriction)
269
269
  * **Flexible fonts**: Any font families allowed (unlike RFC 7996 generic-only restriction)
270
270
  * **Flexible styles**: Any CSS styles allowed
271
+ * **Event handlers allowed**: Supports event attributes (`on*`) for interactivity
271
272
  * **Self-contained resources**: All CSS and fonts must be embedded
272
273
  * **Structural compliance**: Proper namespaces and viewBox required
273
274
  * **No external dependencies**: External CSS and fonts strictly prohibited
274
275
 
276
+ **Event Handler Support**:
277
+
278
+ The metanorma profile allows SVG event handler attributes (e.g., `onmouseover`, `onmouseout`, `onfocus`, `onblur`) for interactive elements. This is configured using wildcard attribute patterns:
279
+
280
+ [source,yaml]
281
+ ----
282
+ requirements:
283
+ - type: "AllowedElementsRequirement"
284
+ id: "allowed_elements"
285
+ allowed_attribute_patterns:
286
+ - "on*" # Allow all event handler attributes
287
+ ----
288
+
275
289
  **Requirements**:
276
290
  [source,yaml]
277
291
  ----
@@ -174,6 +174,62 @@ foreign). Used with `skip_foreign_namespaces`. Default: `[]`.
174
174
  namespaces. When `true`, RDF namespace elements are considered valid and
175
175
  skipped. Default: `false`.
176
176
 
177
+ `allowed_attribute_patterns`:: List of wildcard attribute patterns to exempt from
178
+ validation. Attributes matching any pattern will be allowed regardless of whether
179
+ they appear in element `attributes` lists. Patterns support suffix wildcards (`*`).
180
+ Default: `[]`.
181
+
182
+ * **Common patterns**:
183
+ +
184
+ **`"on*"`**:: Allows all event handler attributes (`onclick`, `onmouseover`, `onmouseout`, etc.)
185
+ **`"data-*"`**:: Allows all `data-` custom attributes
186
+
187
+ * **Precedence**: When an attribute matches both an allowed pattern and an
188
+ element-specific disallowed attribute (prefixed with `!`), the allowed pattern
189
+ takes precedence and a warning is emitted during validation.
190
+
191
+ ==== Wildcard attribute patterns
192
+
193
+ .Using allowed_attribute_patterns to exempt event handler attributes
194
+ [example]
195
+ ====
196
+ [source,yaml]
197
+ ----
198
+ - id: "svg_elements_with_events"
199
+ type: "AllowedElementsRequirement"
200
+ description: "Restrict to allowed SVG elements but allow event handlers"
201
+ allowed_attribute_patterns:
202
+ - "on*" # Allow all event handler attributes
203
+ - "data-*" # Allow all data-* custom attributes
204
+ check_attributes: true
205
+ element_configs:
206
+ - tag: "polygon"
207
+ attributes: ["points", "id", "fill"]
208
+ # !onclick is disallowed, but on* pattern takes precedence with warning
209
+ - tag: "rect"
210
+ attributes: ["x", "y", "width", "height"]
211
+ ----
212
+ ====
213
+
214
+ With this configuration:
215
+ * Event attributes (`onclick`, `onmouseover`, etc.) are allowed on all elements
216
+ * `data-*` custom attributes are allowed on all elements
217
+ * Other attributes not in element `attributes` lists are still rejected
218
+
219
+ .Conflict detection warning
220
+ [example]
221
+ ====
222
+ When an allowed pattern conflicts with an element-specific disallowed attribute
223
+ (prefixed with `!`), a warning is emitted:
224
+
225
+ [source,text]
226
+ ----
227
+ Configuration warning in svg_elements_with_events: Element 'polygon' has
228
+ disallowed attributes [onclick] that match allowed_attribute_patterns [on*].
229
+ Allowed patterns take precedence over element-specific disallowed attributes.
230
+ ----
231
+ ====
232
+
177
233
  ==== Configuration
178
234
 
179
235
  .Example configuration of AllowedElementsRequirement
@@ -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