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