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 +4 -4
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +18 -79
- data/benchmark/performance_comparison.rb +120 -0
- data/config/profiles/metanorma.yml +3 -0
- data/docs/performance.adoc +239 -0
- data/docs/profiles.adoc +14 -0
- data/docs/requirements.adoc +56 -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 +143 -52
- 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
- data/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb +63 -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,80 +1,26 @@
|
|
|
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
|
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
|
8
8
|
|
|
9
|
-
# Offense count:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
84
|
+
# Offense count: 2
|
|
139
85
|
# Configuration parameters: CountBlocks, CountModifierForms.
|
|
140
86
|
Metrics/BlockNesting:
|
|
141
87
|
Max: 4
|
|
142
88
|
|
|
143
|
-
# Offense count:
|
|
89
|
+
# Offense count: 125
|
|
144
90
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
145
91
|
Metrics/CyclomaticComplexity:
|
|
146
92
|
Enabled: false
|
|
147
93
|
|
|
148
|
-
# Offense count:
|
|
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:
|
|
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:
|
|
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
|
----
|
data/docs/requirements.adoc
CHANGED
|
@@ -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
|
-
|
|
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
|
|