svg_conform 0.1.1 → 0.1.2

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +273 -10
  3. data/README.adoc +7 -8
  4. data/examples/readme_usage.rb +67 -0
  5. data/examples/requirements_demo.rb +4 -4
  6. data/lib/svg_conform/document.rb +7 -1
  7. data/lib/svg_conform/element_proxy.rb +101 -0
  8. data/lib/svg_conform/fast_document_analyzer.rb +82 -0
  9. data/lib/svg_conform/node_index_builder.rb +47 -0
  10. data/lib/svg_conform/requirements/allowed_elements_requirement.rb +202 -0
  11. data/lib/svg_conform/requirements/base_requirement.rb +27 -0
  12. data/lib/svg_conform/requirements/color_restrictions_requirement.rb +53 -0
  13. data/lib/svg_conform/requirements/font_family_requirement.rb +18 -0
  14. data/lib/svg_conform/requirements/forbidden_content_requirement.rb +26 -0
  15. data/lib/svg_conform/requirements/id_reference_requirement.rb +96 -0
  16. data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +91 -0
  17. data/lib/svg_conform/requirements/link_validation_requirement.rb +30 -0
  18. data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +59 -0
  19. data/lib/svg_conform/requirements/namespace_requirement.rb +74 -0
  20. data/lib/svg_conform/requirements/no_external_css_requirement.rb +74 -0
  21. data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +58 -0
  22. data/lib/svg_conform/requirements/no_external_images_requirement.rb +40 -0
  23. data/lib/svg_conform/requirements/style_requirement.rb +12 -0
  24. data/lib/svg_conform/requirements/viewbox_required_requirement.rb +72 -0
  25. data/lib/svg_conform/sax_document.rb +46 -0
  26. data/lib/svg_conform/sax_validation_handler.rb +158 -0
  27. data/lib/svg_conform/validation_context.rb +84 -2
  28. data/lib/svg_conform/validator.rb +74 -6
  29. data/lib/svg_conform/version.rb +1 -1
  30. data/lib/svg_conform.rb +1 -0
  31. metadata +8 -2
@@ -45,6 +45,36 @@ module SvgConform
45
45
  end
46
46
  end
47
47
 
48
+ def validate_sax_element(element, context)
49
+ # Check href attributes
50
+ href_value = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
51
+
52
+ if href_value && !ascii_only?(href_value)
53
+ context.add_error(
54
+ requirement_id: id,
55
+ message: "Link href '#{href_value}' contains non-ASCII characters",
56
+ node: element,
57
+ severity: :error
58
+ )
59
+ end
60
+
61
+ # Check other IRI attributes
62
+ iri_attributes = %w[src action formaction cite longdesc usemap]
63
+ iri_attributes.each do |attr_name|
64
+ iri_value = element.raw_attributes[attr_name]
65
+ next unless iri_value
66
+
67
+ next if ascii_only?(iri_value)
68
+
69
+ context.add_error(
70
+ requirement_id: id,
71
+ message: "IRI attribute '#{attr_name}' value '#{iri_value}' contains non-ASCII characters",
72
+ node: element,
73
+ severity: :error
74
+ )
75
+ end
76
+ end
77
+
48
78
  private
49
79
 
50
80
  def ascii_only?(string)
@@ -41,6 +41,65 @@ module SvgConform
41
41
  end
42
42
  end
43
43
 
44
+ def validate_sax_element(element, context)
45
+ # Skip validation for exempt elements (e.g., RDF metadata elements)
46
+ return if exempt_elements.include?(element.name)
47
+
48
+ # Check all attributes for namespace violations
49
+ element.attributes.each do |attr|
50
+ check_sax_attribute(attr, element, context)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def check_sax_attribute(attr, element, context)
57
+ attr_name = attr.name
58
+
59
+ # Check if this is a namespaced attribute by looking for colon in name
60
+ return unless attr_name.include?(":")
61
+
62
+ prefix, = attr_name.split(":", 2)
63
+
64
+ # Find the namespace URI for this prefix by walking up parent chain
65
+ namespace_uri = find_namespace_uri_sax(element, prefix)
66
+ return unless namespace_uri
67
+
68
+ # Determine if this namespace is invalid based on configuration
69
+ invalid_namespace = if allowed_namespaces.empty?
70
+ # Blacklist mode: disallowed namespaces are forbidden
71
+ disallowed_namespaces.include?(namespace_uri)
72
+ else
73
+ # Whitelist mode: only allowed namespaces are permitted
74
+ !allowed_namespaces.include?(namespace_uri)
75
+ end
76
+
77
+ return unless invalid_namespace
78
+
79
+ context.add_error(
80
+ requirement_id: id,
81
+ message: "Element '#{element.name}' does not allow attributes with namespace '#{namespace_uri}'",
82
+ node: element,
83
+ severity: :error,
84
+ data: { attribute: attr_name, namespace: namespace_uri }
85
+ )
86
+ end
87
+
88
+ def find_namespace_uri_sax(element, prefix)
89
+ # Check current element and ancestors for xmlns:prefix declarations
90
+ current = element
91
+ while current
92
+ # Check for xmlns:prefix attribute in raw_attributes
93
+ xmlns_value = current.raw_attributes["xmlns:#{prefix}"]
94
+ return xmlns_value if xmlns_value
95
+
96
+ # Move to parent
97
+ current = current.parent
98
+ end
99
+
100
+ nil
101
+ end
102
+
44
103
  private
45
104
 
46
105
  def check_attribute_nodes(node, context)
@@ -190,8 +190,82 @@ module SvgConform
190
190
  )
191
191
  end
192
192
 
193
+ def validate_sax_element(element, context)
194
+ # Check if this element has a namespace
195
+ element_namespace = get_element_namespace_sax(element)
196
+
197
+ # Skip if no namespace (default SVG namespace)
198
+ return if element_namespace.nil? || element_namespace.empty?
199
+
200
+ # Check against allowed namespaces if configured
201
+ # If allow_rdf_metadata is enabled, also allow RDF namespaces
202
+ effective_allowed_namespaces = allowed_namespaces
203
+ if allow_rdf_metadata
204
+ effective_allowed_namespaces = allowed_namespaces + RDF_NAMESPACES
205
+ end
206
+
207
+ if effective_allowed_namespaces && !effective_allowed_namespaces.empty? && !effective_allowed_namespaces.include?(element_namespace)
208
+ context.add_error(
209
+ requirement_id: id,
210
+ message: "The namespace #{element_namespace} is not permitted for svg elements.",
211
+ node: element,
212
+ severity: :error,
213
+ data: {
214
+ element_name: element.name,
215
+ namespace: element_namespace,
216
+ allowed_namespaces: effective_allowed_namespaces
217
+ }
218
+ )
219
+ return
220
+ end
221
+
222
+ # Check against disallowed namespaces if configured
223
+ return unless disallowed_namespaces && !disallowed_namespaces.empty? && disallowed_namespaces.include?(element_namespace)
224
+
225
+ context.add_error(
226
+ requirement_id: id,
227
+ message: "The namespace #{element_namespace} is not permitted for svg elements.",
228
+ node: element,
229
+ severity: :error,
230
+ data: {
231
+ element_name: element.name,
232
+ namespace: element_namespace,
233
+ disallowed_namespaces: disallowed_namespaces
234
+ }
235
+ )
236
+ end
237
+
193
238
  private
194
239
 
240
+ def get_element_namespace_sax(element)
241
+ # Try to get namespace from the element
242
+ namespace = element.namespace
243
+ return namespace if namespace && !namespace.empty?
244
+
245
+ # If no namespace found, check if element has a prefix (indicating it's namespaced)
246
+ if element.name.include?(":")
247
+ prefix = element.name.split(":").first
248
+ return find_namespace_uri_for_prefix_sax(element, prefix)
249
+ end
250
+
251
+ nil
252
+ end
253
+
254
+ def find_namespace_uri_for_prefix_sax(element, prefix)
255
+ # Check current element and ancestors for namespace declarations
256
+ current = element
257
+ while current
258
+ # Check for xmlns:prefix attribute
259
+ xmlns_attr = "xmlns:#{prefix}"
260
+ return current.raw_attributes[xmlns_attr] if current.raw_attributes[xmlns_attr]
261
+
262
+ # Move to parent
263
+ current = current.parent
264
+ end
265
+
266
+ nil
267
+ end
268
+
195
269
  def check_all_elements(document, context)
196
270
  # Recursively check all elements in the document
197
271
  traverse_elements(document.root, context)
@@ -44,6 +44,17 @@ module SvgConform
44
44
  has_style_attribute?(node)
45
45
  end
46
46
 
47
+ def validate_sax_element(element, context)
48
+ case element.name
49
+ when "style"
50
+ check_style_element_sax(element, context) if check_style_elements
51
+ when "link"
52
+ check_link_element_sax(element, context) if check_link_elements
53
+ else
54
+ check_style_attribute_sax(element, context) if check_style_attributes
55
+ end
56
+ end
57
+
47
58
  private
48
59
 
49
60
  def check_style_element(node, context)
@@ -109,6 +120,69 @@ module SvgConform
109
120
  )
110
121
  end
111
122
 
123
+ def check_style_element_sax(element, context)
124
+ # Check for @import rules in style elements
125
+ content = element.text_content
126
+
127
+ if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
128
+ url = ::Regexp.last_match(1)
129
+ unless allowed_url?(url)
130
+ context.add_error(
131
+ requirement_id: id,
132
+ message: "External CSS import not allowed: #{url}",
133
+ node: element,
134
+ severity: :error
135
+ )
136
+ end
137
+ end
138
+
139
+ return unless content =~ /@import\s+['"]([^'"]+)['"]/i
140
+
141
+ url = ::Regexp.last_match(1)
142
+ return if allowed_url?(url)
143
+
144
+ context.add_error(
145
+ requirement_id: id,
146
+ message: "External CSS import not allowed: #{url}",
147
+ node: element,
148
+ severity: :error
149
+ )
150
+ end
151
+
152
+ def check_link_element_sax(element, context)
153
+ rel = element.raw_attributes["rel"]
154
+ href = element.raw_attributes["href"]
155
+
156
+ return unless rel&.downcase == "stylesheet" && href
157
+
158
+ return if allowed_url?(href)
159
+
160
+ context.add_error(
161
+ requirement_id: id,
162
+ message: "External CSS link not allowed: #{href}",
163
+ node: element,
164
+ severity: :error
165
+ )
166
+ end
167
+
168
+ def check_style_attribute_sax(element, context)
169
+ style_value = element.raw_attributes["style"]
170
+ return unless style_value
171
+
172
+ # Check for url() references in style attributes
173
+ return unless style_value =~ /url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
174
+
175
+ url = ::Regexp.last_match(1)
176
+ return if allowed_url?(url)
177
+
178
+ context.add_error(
179
+ requirement_id: id,
180
+ message: "External URL reference in style attribute not allowed: #{url}",
181
+ node: element,
182
+ severity: :error
183
+ )
184
+ end
185
+
112
186
  def has_style_attribute?(node)
113
187
  !get_attribute(node, "style").nil?
114
188
  end
@@ -41,6 +41,17 @@ module SvgConform
41
41
  has_style_attribute?(node)
42
42
  end
43
43
 
44
+ def validate_sax_element(element, context)
45
+ case element.name
46
+ when "style"
47
+ check_style_element_sax(element, context) if check_style_fonts
48
+ when "font-face"
49
+ check_font_face_element_sax(element, context) if check_font_face
50
+ else
51
+ check_style_attribute_sax(element, context) if check_style_fonts
52
+ end
53
+ end
54
+
44
55
  private
45
56
 
46
57
  def check_style_element(node, context)
@@ -100,6 +111,53 @@ module SvgConform
100
111
  )
101
112
  end
102
113
 
114
+ def check_style_element_sax(element, context)
115
+ # Check for @font-face with external src in style elements
116
+ content = element.text_content
117
+
118
+ # Match @font-face blocks
119
+ content.scan(/@font-face\s*\{([^}]+)\}/m) do |match|
120
+ font_face_content = match[0]
121
+
122
+ # Check for src with url() that is not data: URI
123
+ if font_face_content =~ /src\s*:\s*url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
124
+ url = ::Regexp.last_match(1)
125
+ unless embedded_font?(url)
126
+ context.add_error(
127
+ requirement_id: id,
128
+ message: "External font reference not allowed: #{url}. Fonts must be embedded as data URIs.",
129
+ node: element,
130
+ severity: :error
131
+ )
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def check_font_face_element_sax(element, context)
138
+ # Note: For SAX, we can't traverse children yet
139
+ # This would need to be handled differently or deferred
140
+ # For now, skip XPath-based checking in SAX mode
141
+ end
142
+
143
+ def check_style_attribute_sax(element, context)
144
+ style_value = element.raw_attributes["style"]
145
+ return unless style_value
146
+
147
+ # Check for font-family with url() references
148
+ return unless style_value =~ /font-family\s*:\s*.*url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
149
+
150
+ url = ::Regexp.last_match(1)
151
+ return if embedded_font?(url)
152
+
153
+ context.add_error(
154
+ requirement_id: id,
155
+ message: "External font URL in style attribute not allowed: #{url}. Fonts must be embedded as data URIs.",
156
+ node: element,
157
+ severity: :error
158
+ )
159
+ end
160
+
103
161
  def has_style_attribute?(node)
104
162
  !get_attribute(node, "style").nil?
105
163
  end
@@ -37,6 +37,15 @@ module SvgConform
37
37
  node.name == "image" || has_style_attribute?(node)
38
38
  end
39
39
 
40
+ def validate_sax_element(element, context)
41
+ case element.name
42
+ when "image"
43
+ check_image_element_sax(element, context) if check_image_elements
44
+ else
45
+ check_style_attribute_sax(element, context) if check_style_images
46
+ end
47
+ end
48
+
40
49
  private
41
50
 
42
51
  def check_image_element(node, context)
@@ -70,6 +79,37 @@ module SvgConform
70
79
  end
71
80
  end
72
81
 
82
+ def check_image_element_sax(element, context)
83
+ # Check href and xlink:href attributes
84
+ href = element.raw_attributes["href"] || element.raw_attributes["xlink:href"]
85
+ return unless href && !embedded_image?(href)
86
+
87
+ context.add_error(
88
+ requirement_id: id,
89
+ message: "External image reference not allowed: #{href}. Images must be embedded as data URIs.",
90
+ node: element,
91
+ severity: :error
92
+ )
93
+ end
94
+
95
+ def check_style_attribute_sax(element, context)
96
+ style_value = element.raw_attributes["style"]
97
+ return unless style_value
98
+
99
+ # Check for url() references to images in background, background-image, etc.
100
+ style_value.scan(/url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i) do
101
+ url = ::Regexp.last_match(1)
102
+ next if embedded_image?(url)
103
+
104
+ context.add_error(
105
+ requirement_id: id,
106
+ message: "External image URL in style attribute not allowed: #{url}. Images must be embedded as data URIs.",
107
+ node: element,
108
+ severity: :error
109
+ )
110
+ end
111
+ end
112
+
73
113
  def has_style_attribute?(node)
74
114
  !get_attribute(node, "style").nil?
75
115
  end
@@ -43,6 +43,18 @@ module SvgConform
43
43
  check_style_properties(style_value, node, context)
44
44
  end
45
45
 
46
+ def validate_sax_element(element, context)
47
+ style_value = element.raw_attributes["style"]
48
+ return unless style_value
49
+ return if style_value.strip.empty?
50
+
51
+ # 1. Check for malformed style syntax
52
+ check_malformed_syntax(style_value, element, context)
53
+
54
+ # 2. Check for allowed/disallowed properties and validate their values
55
+ check_style_properties(style_value, element, context)
56
+ end
57
+
46
58
  private
47
59
 
48
60
  def check_malformed_syntax(style_value, node, context)
@@ -83,6 +83,78 @@ module SvgConform
83
83
  )
84
84
  end
85
85
 
86
+ def validate_sax_element(element, context)
87
+ # Only check the root SVG element
88
+ return unless element.name == "svg" && element.parent.nil?
89
+
90
+ viewbox = element.raw_attributes["viewBox"]
91
+
92
+ if viewbox.nil? || viewbox.empty?
93
+ context.add_error(
94
+ requirement_id: id,
95
+ message: "SVG root element must have a viewBox attribute",
96
+ node: element,
97
+ severity: :error,
98
+ data: { missing_attribute: "viewBox" }
99
+ )
100
+
101
+ # Add informational message about calculated viewBox if width/height are present
102
+ width = element.raw_attributes["width"]
103
+ height = element.raw_attributes["height"]
104
+
105
+ if width && height && valid_number?(width) && valid_number?(height)
106
+ calculated_viewbox = "0 0 #{width.to_f} #{height.to_f}"
107
+ context.add_error(
108
+ requirement_id: id,
109
+ message: "Trying to put in the attribute with value '#{calculated_viewbox}'",
110
+ node: element,
111
+ severity: :error,
112
+ data: {
113
+ calculated_viewbox: calculated_viewbox,
114
+ source_width: width,
115
+ source_height: height
116
+ }
117
+ )
118
+ end
119
+ return
120
+ end
121
+
122
+ # Validate viewBox format (should be "min-x min-y width height")
123
+ normalized_viewbox = viewbox.strip.gsub(/[(),]/, " ").squeeze(" ")
124
+ parts = normalized_viewbox.strip.split(/\s+/)
125
+ unless parts.length == 4 && parts.all? { |part| valid_number?(part) }
126
+ context.add_error(
127
+ requirement_id: id,
128
+ message: "viewBox attribute must contain four numeric values (min-x min-y width height)",
129
+ node: element,
130
+ severity: :error,
131
+ data: {
132
+ viewbox_value: viewbox,
133
+ parsed_parts: parts
134
+ }
135
+ )
136
+ return
137
+ end
138
+
139
+ # Check that width and height are positive
140
+ width = parts[2].to_f
141
+ height = parts[3].to_f
142
+
143
+ return unless width <= 0 || height <= 0
144
+
145
+ context.add_error(
146
+ requirement_id: id,
147
+ message: "viewBox width and height must be positive values",
148
+ node: element,
149
+ severity: :error,
150
+ data: {
151
+ viewbox_value: viewbox,
152
+ width: width,
153
+ height: height
154
+ }
155
+ )
156
+ end
157
+
86
158
  private
87
159
 
88
160
  def valid_number?(str)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "sax_validation_handler"
5
+
6
+ module SvgConform
7
+ # SAX-based document for streaming validation
8
+ # Provides high-performance validation for large SVG files
9
+ # without loading entire DOM tree into memory
10
+ class SaxDocument
11
+ attr_reader :file_path, :content
12
+
13
+ def self.from_file(file_path)
14
+ new(File.read(file_path), file_path)
15
+ end
16
+
17
+ def self.from_content(content)
18
+ new(content, nil)
19
+ end
20
+
21
+ def initialize(content, file_path = nil)
22
+ @content = content
23
+ @file_path = file_path
24
+ end
25
+
26
+ # Validate using SAX streaming parser
27
+ def validate_with_profile(profile)
28
+ handler = SaxValidationHandler.new(profile)
29
+ parser = Nokogiri::XML::SAX::Parser.new(handler)
30
+
31
+ begin
32
+ parser.parse(@content)
33
+ rescue StandardError => e
34
+ # Handle parse errors
35
+ handler.add_parse_error(e)
36
+ end
37
+
38
+ handler.result
39
+ end
40
+
41
+ # For compatibility - convert to DOM when needed
42
+ def to_dom
43
+ Document.from_content(@content)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "element_proxy"
5
+ require_relative "validation_context"
6
+ require_relative "validation_result"
7
+
8
+ module SvgConform
9
+ # SAX event handler for streaming SVG validation
10
+ # Processes XML events and dispatches to requirements
11
+ class SaxValidationHandler < Nokogiri::XML::SAX::Document
12
+ attr_reader :result, :context
13
+
14
+ def initialize(profile)
15
+ @profile = profile
16
+ @element_stack = [] # Track parent-child hierarchy
17
+ @path_stack = [] # Current element path
18
+ @position_counters = [] # Stack of sibling counters per level
19
+ @parse_errors = []
20
+ @result = nil # Will be set in end_document
21
+
22
+ # Create validation context (without document reference for SAX)
23
+ @context = create_sax_context
24
+
25
+ # Classify requirements into immediate vs deferred
26
+ @immediate_requirements = []
27
+ @deferred_requirements = []
28
+ classify_requirements
29
+ end
30
+
31
+ # SAX Event: Document start
32
+ def start_document
33
+ # Initialize root level counters
34
+ @position_counters.push({})
35
+ end
36
+
37
+ # SAX Event: Element start tag
38
+ def start_element(name, attributes = [])
39
+ attrs = Hash[attributes]
40
+
41
+ # Calculate position among siblings at current level
42
+ current_counters = @position_counters.last || {}
43
+ current_counters[name] ||= 0
44
+ current_counters[name] += 1
45
+ position = current_counters[name]
46
+
47
+ # Build element proxy
48
+ element = ElementProxy.new(
49
+ name: name,
50
+ attributes: attrs,
51
+ position: position,
52
+ path: @path_stack.dup,
53
+ parent: @element_stack.last
54
+ )
55
+
56
+ # Push to stacks
57
+ @element_stack.push(element)
58
+ @path_stack.push("#{name}[#{position}]")
59
+ @position_counters.push({}) # New level for this element's children
60
+
61
+ # Validate with immediate requirements
62
+ @immediate_requirements.each do |req|
63
+ req.validate_sax_element(element, @context)
64
+ end
65
+
66
+ # Deferred requirements may need to collect data
67
+ @deferred_requirements.each do |req|
68
+ req.collect_sax_data(element, @context) if req.respond_to?(:collect_sax_data)
69
+ end
70
+ end
71
+
72
+ # SAX Event: Element end tag
73
+ def end_element(name)
74
+ @element_stack.pop
75
+ @path_stack.pop
76
+ @position_counters.pop
77
+ end
78
+
79
+ # SAX Event: Text content
80
+ def characters(string)
81
+ return if @element_stack.empty?
82
+ @element_stack.last.text_content << string
83
+ end
84
+
85
+ # SAX Event: Document complete
86
+ def end_document
87
+ # Run deferred validation
88
+ @deferred_requirements.each do |req|
89
+ req.validate_sax_complete(@context)
90
+ end
91
+
92
+ # Create result
93
+ @result = ValidationResult.new(nil, @profile, @context)
94
+ end
95
+
96
+ # SAX Event: Parse error
97
+ def error(error_message)
98
+ @parse_errors << error_message
99
+ end
100
+
101
+ # SAX Event: Warning
102
+ def warning(warning_message)
103
+ # Can log warnings if needed
104
+ end
105
+
106
+ # Handle parse errors
107
+ def add_parse_error(error)
108
+ @context.add_error(
109
+ node: nil,
110
+ message: "Parse error: #{error.message}",
111
+ requirement_id: "parse_error",
112
+ severity: :error
113
+ )
114
+ end
115
+
116
+ # Get result (will be nil until end_document called)
117
+ def result
118
+ @result || create_incomplete_result
119
+ end
120
+
121
+ private
122
+
123
+ def create_incomplete_result
124
+ # Return result even if parsing incomplete
125
+ ValidationResult.new(nil, @profile, @context)
126
+ end
127
+
128
+ # Create a SAX-compatible validation context
129
+ def create_sax_context
130
+ # Create context without triggering DOM operations
131
+ context = ValidationContext.allocate
132
+ context.instance_variable_set(:@document, nil)
133
+ context.instance_variable_set(:@profile, @profile)
134
+ context.instance_variable_set(:@errors, [])
135
+ context.instance_variable_set(:@warnings, [])
136
+ context.instance_variable_set(:@validity_errors, [])
137
+ context.instance_variable_set(:@infos, [])
138
+ context.instance_variable_set(:@data, {})
139
+ context.instance_variable_set(:@structurally_invalid_node_ids, Set.new)
140
+ context.instance_variable_set(:@node_id_cache, {})
141
+ context.instance_variable_set(:@cache_populated, true) # Skip population for SAX
142
+ context
143
+ end
144
+
145
+ # Classify requirements based on validation needs
146
+ def classify_requirements
147
+ return unless @profile.requirements
148
+
149
+ @profile.requirements.each do |req|
150
+ if req.respond_to?(:needs_deferred_validation?) && req.needs_deferred_validation?
151
+ @deferred_requirements << req
152
+ else
153
+ @immediate_requirements << req
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end