svg_conform 0.1.4 → 0.1.5

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +182 -21
  3. data/README.adoc +391 -989
  4. data/config/profiles/metanorma.yml +5 -0
  5. data/docs/api_reference.adoc +1355 -0
  6. data/docs/cli_guide.adoc +846 -0
  7. data/docs/reference_manifest.adoc +370 -0
  8. data/docs/requirements.adoc +68 -1
  9. data/examples/document_input_demo.rb +102 -0
  10. data/lib/svg_conform/document.rb +40 -1
  11. data/lib/svg_conform/profile.rb +15 -9
  12. data/lib/svg_conform/references/base_reference.rb +130 -0
  13. data/lib/svg_conform/references/id_definition.rb +38 -0
  14. data/lib/svg_conform/references/reference_classifier.rb +45 -0
  15. data/lib/svg_conform/references/reference_manifest.rb +129 -0
  16. data/lib/svg_conform/references.rb +11 -0
  17. data/lib/svg_conform/remediations/namespace_attribute_remediation.rb +34 -43
  18. data/lib/svg_conform/requirements/id_collection_requirement.rb +38 -0
  19. data/lib/svg_conform/requirements/id_reference_requirement.rb +11 -0
  20. data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +3 -0
  21. data/lib/svg_conform/requirements/link_validation_requirement.rb +114 -31
  22. data/lib/svg_conform/requirements/no_external_css_requirement.rb +5 -2
  23. data/lib/svg_conform/requirements.rb +11 -9
  24. data/lib/svg_conform/sax_validation_handler.rb +16 -1
  25. data/lib/svg_conform/validation_context.rb +67 -1
  26. data/lib/svg_conform/validation_result.rb +43 -2
  27. data/lib/svg_conform/validator.rb +56 -16
  28. data/lib/svg_conform/version.rb +1 -1
  29. data/lib/svg_conform.rb +11 -2
  30. data/spec/svg_conform/commands/svgcheck_compare_command_spec.rb +1 -0
  31. data/spec/svg_conform/commands/svgcheck_compatibility_command_spec.rb +1 -0
  32. data/spec/svg_conform/commands/svgcheck_generate_command_spec.rb +1 -0
  33. data/spec/svg_conform/references/integration_spec.rb +206 -0
  34. data/spec/svg_conform/references/reference_classifier_spec.rb +142 -0
  35. data/spec/svg_conform/references/reference_manifest_spec.rb +307 -0
  36. data/spec/svg_conform/requirements/id_reference_state_spec.rb +93 -0
  37. data/spec/svg_conform/validator_input_types_spec.rb +172 -0
  38. metadata +17 -2
@@ -0,0 +1,370 @@
1
+ = Reference Manifest and External Reference Handling
2
+ :toc:
3
+ :toclevels: 3
4
+
5
+ == Overview
6
+
7
+ The Reference Manifest provides a comprehensive view of all IDs and references in an SVG document, enabling consumers like Metanorma to make informed validation decisions about external references in their own context.
8
+
9
+ == Architecture
10
+
11
+ === Reference Classification
12
+
13
+ References are classified by validation scope:
14
+
15
+ * **Internal References**: Can be validated by svg_conform (e.g., `#element-id`)
16
+ * **External References**: Require consumer-level validation (e.g., URLs, URNs)
17
+
18
+ .Reference Type Hierarchy
19
+ [source]
20
+ ----
21
+ BaseReference (abstract)
22
+ ├── InternalFragmentReference (#element-id) - Internal validation
23
+ ├── DataUriReference (data:*) - Internal validation
24
+ ├── ExternalUrlReference (http://, https://) - Consumer validation
25
+ ├── UrnReference (urn:*) - Consumer validation
26
+ └── RelativePathReference (./path, /path) - Consumer validation
27
+ ----
28
+
29
+ === ReferenceManifest Components
30
+
31
+ The manifest provides complete context:
32
+
33
+ * **Available IDs**: All IDs defined in the document
34
+ * **Internal References**: Fragment identifiers referencing document IDs
35
+ * **External References**: URLs, URNs, and paths requiring consumer validation
36
+
37
+ == Usage
38
+
39
+ === Basic Usage
40
+
41
+ [source,ruby]
42
+ ----
43
+ require 'svg_conform'
44
+
45
+ validator = SvgConform::Validator.new(profile: 'metanorma')
46
+ result = validator.validate(svg_content)
47
+
48
+ # Access the reference manifest
49
+ manifest = result.reference_manifest
50
+
51
+ # Check statistics
52
+ stats = manifest.statistics
53
+ puts "Total IDs: #{stats[:total_ids]}"
54
+ puts "External references: #{stats[:external_references]}"
55
+ ----
56
+
57
+ === Accessing IDs
58
+
59
+ [source,ruby]
60
+ ----
61
+ # Get all defined IDs
62
+ manifest.available_ids.each do |id_def|
63
+ puts "ID: #{id_def.id_value} on element #{id_def.element_name}"
64
+ puts " at line #{id_def.line_number}" if id_def.line_number
65
+ end
66
+
67
+ # Check if specific ID exists
68
+ if manifest.id_defined?('target-element')
69
+ puts "ID 'target-element' is defined"
70
+ end
71
+ ----
72
+
73
+ === Accessing References
74
+
75
+ [source,ruby]
76
+ ----
77
+ # Get all internal references
78
+ manifest.internal_references.each do |ref|
79
+ puts "Internal ref: #{ref.value} from #{ref.element_name}"
80
+ end
81
+
82
+ # Get all external references
83
+ manifest.external_references.each do |ref|
84
+ puts "External ref: #{ref.value} from #{ref.element_name}"
85
+ puts " Requires consumer validation"
86
+ end
87
+
88
+ # Get references by type
89
+ manifest.references_by_type.each do |type, refs|
90
+ puts "#{type}: #{refs.size} references"
91
+ end
92
+ ----
93
+
94
+ === Validating External References
95
+
96
+ [source,ruby]
97
+ ----
98
+ # Validate URN references in consumer context
99
+ manifest.external_references
100
+ .select { |ref| ref.is_a?(SvgConform::References::UrnReference) }
101
+ .each do |urn_ref|
102
+ namespace = urn_ref.namespace
103
+
104
+ unless allowed_urn_namespaces.include?(namespace)
105
+ puts "Error: Disallowed URN namespace '#{namespace}'"
106
+ puts " at line #{urn_ref.line_number}" if urn_ref.line_number
107
+ end
108
+ end
109
+
110
+ # Validate external URLs
111
+ manifest.external_references
112
+ .select { |ref| ref.is_a?(SvgConform::References::ExternalUrlReference) }
113
+ .each do |url_ref|
114
+ unless url_accessible?(url_ref.value)
115
+ puts "Warning: URL may be inaccessible: #{url_ref.value}"
116
+ end
117
+ end
118
+ ----
119
+
120
+ === Query Interface
121
+
122
+ [source,ruby]
123
+ ----
124
+ # Find references to a specific ID
125
+ refs = manifest.references_to_id('target-element')
126
+ puts "Found #{refs.size} references to 'target-element'"
127
+
128
+ # Check for unresolved internal references
129
+ unresolved = manifest.unresolved_internal_references
130
+ if unresolved.any?
131
+ puts "Unresolved references:"
132
+ unresolved.each do |ref|
133
+ puts " #{ref.value} at line #{ref.line_number}"
134
+ end
135
+ end
136
+ ----
137
+
138
+ === Cross-Document Validation
139
+
140
+ [source,ruby]
141
+ ----
142
+ # Build manifests for multiple documents
143
+ manifests = svg_files.map do |file|
144
+ result = validator.validate_file(file)
145
+ result.reference_manifest
146
+ end
147
+
148
+ # Validate cross-document references
149
+ manifests.each do |manifest|
150
+ manifest.external_references.each do |ref|
151
+ # Parse reference like "other-doc.svg#element-42"
152
+ if ref.value =~ %r{^(.+\.svg)#(.+)$}
153
+ target_doc = $1
154
+ target_id = $2
155
+
156
+ # Find target manifest
157
+ target = manifests.find { |m| m.source_document.end_with?(target_doc) }
158
+
159
+ unless target&.id_defined?(target_id)
160
+ puts "Error: Cross-document reference to non-existent ID"
161
+ puts " #{ref.value} at line #{ref.line_number}"
162
+ end
163
+ end
164
+ end
165
+ end
166
+ ----
167
+
168
+ === Export Formats
169
+
170
+ [source,ruby]
171
+ ----
172
+ # Export as YAML for human inspection
173
+ File.write('manifest.yml', result.export_manifest(format: :yaml))
174
+
175
+ # Export as JSON for programmatic processing
176
+ File.write('manifest.json', result.export_manifest(format: :json))
177
+
178
+ # Get as Hash for in-memory processing
179
+ manifest_hash = result.export_manifest(format: :hash)
180
+ ----
181
+
182
+ == Reference Types
183
+
184
+ === InternalFragmentReference
185
+
186
+ Internal SVG element references (e.g., `#element-id`).
187
+
188
+ **Validation Scope**: Internal - validated by svg_conform
189
+
190
+ **Methods**:
191
+
192
+ * `target_id` - Returns the target ID without the `#` prefix
193
+
194
+ [source,ruby]
195
+ ----
196
+ ref = manifest.internal_references.first
197
+ puts ref.target_id # => "element-id" (without #)
198
+ ----
199
+
200
+ === ExternalUrlReference
201
+
202
+ HTTP/HTTPS URL references.
203
+
204
+ **Validation Scope**: External - requires consumer validation
205
+
206
+ **Methods**:
207
+
208
+ * `protocol` - Returns the URL protocol (`http` or `https`)
209
+
210
+ [source,ruby]
211
+ ----
212
+ ref = manifest.external_references.find { |r| r.is_a?(SvgConform::References::ExternalUrlReference) }
213
+ puts ref.protocol # => "https"
214
+ ----
215
+
216
+ === UrnReference
217
+
218
+ URN (Uniform Resource Name) references (e.g., `urn:ietf:rfc:7996`).
219
+
220
+ **Validation Scope**: External - requires consumer validation
221
+
222
+ **Methods**:
223
+
224
+ * `namespace` - Returns the URN namespace
225
+
226
+ [source,ruby]
227
+ ----
228
+ ref = manifest.external_references.find { |r| r.is_a?(SvgConform::References::UrnReference) }
229
+ puts ref.namespace # => "ietf"
230
+ ----
231
+
232
+ === RelativePathReference
233
+
234
+ Relative path references (e.g., `./other.svg`, `/absolute/path.svg`).
235
+
236
+ **Validation Scope**: External - requires consumer validation
237
+
238
+ **Methods**:
239
+
240
+ * `has_fragment?` - Returns true if reference includes fragment (`#`)
241
+ * `path_component` - Returns path portion before fragment
242
+ * `fragment_component` - Returns fragment portion after `#`
243
+
244
+ [source,ruby]
245
+ ----
246
+ ref = SvgConform::References::RelativePathReference.new(
247
+ value: "./other.svg#element"
248
+ )
249
+ puts ref.has_fragment? # => true
250
+ puts ref.path_component # => "./other.svg"
251
+ puts ref.fragment_component # => "element"
252
+ ----
253
+
254
+ === DataUriReference
255
+
256
+ Data URI references (e.g., `data:image/png;base64,iVBORw0KGgo=`).
257
+
258
+ **Validation Scope**: Internal - validated by svg_conform
259
+
260
+ **Methods**:
261
+
262
+ * `media_type` - Returns the media type from data URI
263
+
264
+ [source,ruby]
265
+ ----
266
+ ref = manifest.internal_references.find { |r| r.is_a?(SvgConform::References::DataUriReference) }
267
+ puts ref.media_type # => "image/png"
268
+ ----
269
+
270
+ == Integration with ValidationResult
271
+
272
+ The manifest is automatically included in validation results:
273
+
274
+ [source,ruby]
275
+ ----
276
+ result = validator.validate(svg_content)
277
+
278
+ # Convenience accessors
279
+ result.available_ids # All defined IDs
280
+ result.internal_references # Internal references
281
+ result.external_references # External references
282
+ result.has_external_references? # Boolean check
283
+ result.unresolved_internal_references # Broken internal refs
284
+ result.reference_statistics # Statistics hash
285
+
286
+ # Export manifest
287
+ result.export_manifest(format: :yaml)
288
+
289
+ # Full result includes manifest
290
+ result_hash = result.to_h
291
+ puts result_hash[:reference_manifest]
292
+ ----
293
+
294
+ == Metanorma Integration Example
295
+
296
+ [source,ruby]
297
+ ----
298
+ # Validate SVG document
299
+ result = validator.validate(svg_content, profile: 'metanorma')
300
+
301
+ # Get external references for consumer validation
302
+ external_refs = result.external_references
303
+
304
+ # Validate each reference type in Metanorma's context
305
+ external_refs.each do |ref|
306
+ case ref
307
+ when SvgConform::References::UrnReference
308
+ # Validate URN resolves in Metanorma's registry
309
+ unless metanorma_urn_registry.resolve(ref.value)
310
+ metanorma_reporter.error(
311
+ "Unresolved URN: #{ref.value}",
312
+ line: ref.line_number
313
+ )
314
+ end
315
+
316
+ when SvgConform::References::ExternalUrlReference
317
+ # Optionally verify URL accessibility
318
+ if metanorma_config.verify_urls?
319
+ unless url_checker.accessible?(ref.value)
320
+ metanorma_reporter.warning(
321
+ "URL may not be accessible: #{ref.value}",
322
+ line: ref.line_number
323
+ )
324
+ end
325
+ end
326
+
327
+ when SvgConform::References::RelativePathReference
328
+ # Resolve relative path and verify file exists
329
+ resolved_path = resolve_relative_to_document(ref.value)
330
+ unless File.exist?(resolved_path)
331
+ metanorma_reporter.error(
332
+ "Referenced file not found: #{ref.value}",
333
+ line: ref.line_number
334
+ )
335
+ end
336
+ end
337
+ end
338
+
339
+ # Report summary
340
+ puts "External references requiring validation: #{external_refs.size}"
341
+ puts "By type: #{result.reference_statistics[:references_by_type]}"
342
+ ----
343
+
344
+ == Benefits
345
+
346
+ === Complete Context
347
+
348
+ The manifest provides both IDs and references, enabling comprehensive cross-validation.
349
+
350
+ === Consumer Control
351
+
352
+ Consumers decide validation rules for external references based on their own context.
353
+
354
+ === Non-Breaking
355
+
356
+ Existing svg_conform validation unchanged; external references reported but not errored.
357
+
358
+ === Type-Safe
359
+
360
+ Strongly-typed reference objects with validation scope clearly defined.
361
+
362
+ === Extensible
363
+
364
+ Easy to add new reference types without modifying existing code.
365
+
366
+ == See Also
367
+
368
+ * link:../EXTERNAL_REFERENCE_PROPOSAL.md[External Reference Architecture Proposal]
369
+ * link:requirements.adoc[Requirements Documentation]
370
+ * link:../config/profiles/metanorma.yml[Metanorma Profile Configuration]
@@ -873,15 +873,82 @@ elements within the document.
873
873
  * link:remediation.adoc#invalid-id-references-remediation[InvalidIdReferencesRemediation]
874
874
 
875
875
 
876
+ [[id-collection-requirement]]
877
+ === ID collection requirement
878
+
879
+ ==== General
880
+
881
+ Collects all ID definitions in the document to build a comprehensive reference
882
+ manifest for validation and consumer-level processing.
883
+
884
+ Implemented as `IdCollectionRequirement`.
885
+
886
+ ==== Configuration options
887
+
888
+ No configuration options. This requirement automatically collects all IDs during validation.
889
+
890
+ ==== Configuration
891
+
892
+ .Example configuration of IdCollectionRequirement
893
+ [example]
894
+ ====
895
+ [source,yaml]
896
+ ----
897
+ - id: "id_collection"
898
+ type: "IdCollectionRequirement"
899
+ description: "Collects all ID definitions for reference validation"
900
+ ----
901
+ ====
902
+
903
+ ==== Example from metanorma profile
904
+
905
+ [example]
906
+ ====
907
+ [source,yaml]
908
+ ----
909
+ - id: "id_collection"
910
+ type: "IdCollectionRequirement"
911
+ description: "Collects all ID definitions for reference validation"
912
+ ----
913
+
914
+ The metanorma profile uses this requirement as the first requirement to build
915
+ the ID manifest before other requirements validate references.
916
+ ====
917
+
918
+ ==== Features
919
+
920
+ * Tracks all ID definitions with location information (line/column numbers)
921
+ * Builds reference manifest for cross-validation
922
+ * Enables consumer-level validation of external references
923
+ * Works in both DOM and SAX validation modes
924
+ * Zero overhead - collects during normal traversal
925
+
926
+ ==== Related features
927
+
928
+ * Reference Manifest - Complete context of IDs and references
929
+ * Link Validation - Uses collected IDs for validation
930
+
931
+
876
932
  [[link-validation-requirement]]
877
933
  === Link validation requirement
878
934
 
879
935
  ==== General
880
936
 
881
- Validates that links use only ASCII characters (IETF requirement).
937
+ Validates that links use only ASCII characters (IETF requirement) and classifies
938
+ references by validation scope for consumer-level processing.
882
939
 
883
940
  Implemented as `LinkValidationRequirement`.
884
941
 
942
+ ==== Reference classification
943
+
944
+ The requirement classifies references into types based on validation scope:
945
+
946
+ * **Internal references**: Validated by svg_conform (e.g., `#element-id`, `data:` URIs)
947
+ * **External references**: Deferred to consumer validation (e.g., URNs, URLs, relative paths)
948
+
949
+ This enables consumers like Metanorma to validate external references in their
950
+ own context rather than having svg_conform report them as errors.
951
+
885
952
  ==== Configuration options
886
953
 
887
954
  `ascii_only`:: Boolean flag to restrict links to ASCII characters. Default:
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "svg_conform"
6
+ require "nokogiri"
7
+ require "moxml"
8
+
9
+ # Sample SVG content
10
+ svg_content = <<~SVG
11
+ <?xml version="1.0" encoding="UTF-8"?>
12
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
13
+ <defs>
14
+ <rect id="box" width="10" height="10"/>
15
+ </defs>
16
+ <use href="#box" x="20" y="20" fill="black"/>
17
+ </svg>
18
+ SVG
19
+
20
+ validator = SvgConform::Validator.new(mode: :sax)
21
+
22
+ puts "=" * 60
23
+ puts "SvgConform: Document Object Input Demo"
24
+ puts "=" * 60
25
+
26
+ # 1. String input (backward compatible)
27
+ puts "\n1. String Input (Traditional)"
28
+ puts "-" * 60
29
+ result = validator.validate(svg_content, profile: :metanorma)
30
+ puts "Valid: #{result.valid?}"
31
+ puts "Errors: #{result.errors.size}"
32
+ puts "Available IDs: #{result.available_ids.map(&:id_value).join(', ')}"
33
+
34
+ # 2. Moxml Document input
35
+ puts "\n2. Moxml Document Input"
36
+ puts "-" * 60
37
+ moxml_doc = Moxml.new.parse(svg_content)
38
+ result = validator.validate(moxml_doc, profile: :metanorma)
39
+ puts "Valid: #{result.valid?}"
40
+ puts "Errors: #{result.errors.size}"
41
+ puts "Available IDs: #{result.available_ids.map(&:id_value).join(', ')}"
42
+
43
+ # 3. Moxml Element input
44
+ puts "\n3. Moxml Element Input"
45
+ puts "-" * 60
46
+ moxml_element = moxml_doc.root
47
+ result = validator.validate(moxml_element, profile: :metanorma)
48
+ puts "Valid: #{result.valid?}"
49
+ puts "Available IDs: #{result.available_ids.map(&:id_value).join(', ')}"
50
+
51
+ # 4. Nokogiri Document input
52
+ puts "\n4. Nokogiri Document Input"
53
+ puts "-" * 60
54
+ nokogiri_doc = Nokogiri::XML(svg_content)
55
+ result = validator.validate(nokogiri_doc, profile: :metanorma)
56
+ puts "Valid: #{result.valid?}"
57
+ puts "Available IDs: #{result.available_ids.map(&:id_value).join(', ')}"
58
+
59
+ # 5. Nokogiri Element input (Metanorma use case)
60
+ puts "\n5. Nokogiri Element Input (Metanorma Integration)"
61
+ puts "-" * 60
62
+ nokogiri_element = nokogiri_doc.root
63
+ puts "Element class: #{nokogiri_element.class.name}"
64
+ result = validator.validate(nokogiri_element, profile: :metanorma)
65
+ puts "Valid: #{result.valid?}"
66
+ puts "Available IDs: #{result.available_ids.map(&:id_value).join(', ')}"
67
+
68
+ # 6. Performance comparison
69
+ puts "\n6. Performance Comparison"
70
+ puts "-" * 60
71
+ puts "Old way (metanorma):"
72
+ puts " nokogiri_element.to_xml → validator.validate(string)"
73
+ puts " = TWO conversions (serialize + parse)"
74
+ puts
75
+ puts "New way:"
76
+ puts " validator.validate(nokogiri_element)"
77
+ puts " = ONE conversion (serialize for SAX only)"
78
+ puts
79
+ puts "Benefit: 50% reduction in serialization/parsing overhead!"
80
+
81
+ # 7. Reference manifest with document input
82
+ puts "\n7. Reference Manifest from Document Objects"
83
+ puts "-" * 60
84
+ result = validator.validate(nokogiri_element, profile: :metanorma)
85
+ manifest = result.reference_manifest
86
+
87
+ puts "Total IDs defined: #{manifest.available_ids.size}"
88
+ puts "Internal references: #{manifest.internal_references.size}"
89
+ puts "External references: #{manifest.external_references.size}"
90
+
91
+ if result.unresolved_internal_references.any?
92
+ puts "\nUnresolved references:"
93
+ result.unresolved_internal_references.each do |ref|
94
+ puts " - #{ref.value}"
95
+ end
96
+ else
97
+ puts "\nAll internal references resolved ✓"
98
+ end
99
+
100
+ puts "\n#{'=' * 60}"
101
+ puts "Demo complete!"
102
+ puts "=" * 60
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "moxml"
4
+ require "nokogiri" if defined?(Nokogiri)
4
5
 
5
6
  module SvgConform
6
7
  # Wrapper around Moxml document for SVG validation
@@ -28,6 +29,40 @@ module SvgConform
28
29
  new(content)
29
30
  end
30
31
 
32
+ # Create Document from an already-parsed node (Nokogiri or Moxml)
33
+ # Avoids re-parsing when input is already a DOM object
34
+ def self.from_node(node)
35
+ doc = allocate
36
+ doc.instance_variable_set(:@file_path, nil)
37
+ doc.instance_variable_set(:@xpath_cache, {})
38
+
39
+ # Convert node to Moxml document
40
+ if node.respond_to?(:to_xml) && node.class.name.start_with?("Moxml")
41
+ # Already a Moxml node, wrap it
42
+ doc.instance_variable_set(:@content, nil) # Don't store XML string
43
+ doc.instance_variable_set(:@moxml_document,
44
+ node.is_a?(Moxml::Document) ? node : wrap_moxml_element(node))
45
+ elsif defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
46
+ # Nokogiri node - must serialize to convert to Moxml
47
+ # This is unavoidable due to different DOM implementations
48
+ xml_string = node.to_xml
49
+ doc.instance_variable_set(:@content, xml_string)
50
+ doc.send(:parse_document)
51
+ else
52
+ raise ArgumentError,
53
+ "Invalid input type: #{node.class}. Expected Moxml or Nokogiri DOM node."
54
+ end
55
+
56
+ doc
57
+ end
58
+
59
+ def self.wrap_moxml_element(element)
60
+ # If it's an element, we need to wrap it in a document
61
+ # For now, parse its XML representation
62
+ context = Moxml.new
63
+ context.parse(element.to_xml)
64
+ end
65
+
31
66
  def root
32
67
  @moxml_document.root
33
68
  end
@@ -55,6 +90,8 @@ module SvgConform
55
90
  end
56
91
 
57
92
  def to_xml
93
+ # Always generate from current moxml_document state
94
+ # This ensures modifications are reflected in the output
58
95
  @moxml_document.to_xml
59
96
  end
60
97
 
@@ -81,7 +118,9 @@ module SvgConform
81
118
  end
82
119
 
83
120
  def has_svg_namespace_prefix?
84
- @content.include?("xmlns:svg=") || @content.include?("svg:")
121
+ # Check the current document state, not cached content
122
+ xml = @moxml_document.to_xml
123
+ xml.include?("xmlns:svg=") || xml.include?("svg:")
85
124
  end
86
125
 
87
126
  def has_viewbox?
@@ -114,19 +114,25 @@ module SvgConform
114
114
  end
115
115
 
116
116
  def validate(document)
117
- # Use SAX mode for validation performance
118
- # Convert document to content string if it's a DOM document
117
+ # If it's a Document object, it's already loaded into DOM
118
+ # Use DOM validation (safe, already in memory)
119
119
  if document.is_a?(Document)
120
- content = document.to_xml
121
- sax_doc = SaxDocument.from_content(content)
122
- return sax_doc.validate_with_profile(self)
120
+ context = ValidationContext.new(document, self)
121
+
122
+ # Validate using requirements system
123
+ requirements&.each do |requirement|
124
+ requirement.validate_document(document, context)
125
+ end
126
+
127
+ ValidationResult.new(document, self, context)
123
128
  elsif document.respond_to?(:to_xml)
124
- # Handle any document-like object
129
+ # For other document objects, serialize and use SAX for safety
130
+ # (This handles Nokogiri/Moxml documents that aren't wrapped in Document)
125
131
  content = document.to_xml
126
132
  sax_doc = SaxDocument.from_content(content)
127
- return sax_doc.validate_with_profile(self)
133
+ sax_doc.validate_with_profile(self)
128
134
  else
129
- # Fallback to DOM mode for backward compatibility
135
+ # Fallback to DOM mode for non-serializable objects
130
136
  context = ValidationContext.new(document, self)
131
137
 
132
138
  # Validate using requirements system
@@ -134,7 +140,7 @@ module SvgConform
134
140
  requirement.validate_document(document, context)
135
141
  end
136
142
 
137
- return ValidationResult.new(document, self, context)
143
+ ValidationResult.new(document, self, context)
138
144
  end
139
145
  end
140
146