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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +182 -21
- data/README.adoc +391 -989
- data/config/profiles/metanorma.yml +5 -0
- data/docs/api_reference.adoc +1355 -0
- data/docs/cli_guide.adoc +846 -0
- data/docs/reference_manifest.adoc +370 -0
- data/docs/requirements.adoc +68 -1
- data/examples/document_input_demo.rb +102 -0
- data/lib/svg_conform/document.rb +40 -1
- data/lib/svg_conform/profile.rb +15 -9
- data/lib/svg_conform/references/base_reference.rb +130 -0
- data/lib/svg_conform/references/id_definition.rb +38 -0
- data/lib/svg_conform/references/reference_classifier.rb +45 -0
- data/lib/svg_conform/references/reference_manifest.rb +129 -0
- data/lib/svg_conform/references.rb +11 -0
- data/lib/svg_conform/remediations/namespace_attribute_remediation.rb +34 -43
- data/lib/svg_conform/requirements/id_collection_requirement.rb +38 -0
- data/lib/svg_conform/requirements/id_reference_requirement.rb +11 -0
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +3 -0
- data/lib/svg_conform/requirements/link_validation_requirement.rb +114 -31
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +5 -2
- data/lib/svg_conform/requirements.rb +11 -9
- data/lib/svg_conform/sax_validation_handler.rb +16 -1
- data/lib/svg_conform/validation_context.rb +67 -1
- data/lib/svg_conform/validation_result.rb +43 -2
- data/lib/svg_conform/validator.rb +56 -16
- data/lib/svg_conform/version.rb +1 -1
- data/lib/svg_conform.rb +11 -2
- data/spec/svg_conform/commands/svgcheck_compare_command_spec.rb +1 -0
- data/spec/svg_conform/commands/svgcheck_compatibility_command_spec.rb +1 -0
- data/spec/svg_conform/commands/svgcheck_generate_command_spec.rb +1 -0
- data/spec/svg_conform/references/integration_spec.rb +206 -0
- data/spec/svg_conform/references/reference_classifier_spec.rb +142 -0
- data/spec/svg_conform/references/reference_manifest_spec.rb +307 -0
- data/spec/svg_conform/requirements/id_reference_state_spec.rb +93 -0
- data/spec/svg_conform/validator_input_types_spec.rb +172 -0
- 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]
|
data/docs/requirements.adoc
CHANGED
|
@@ -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
|
data/lib/svg_conform/document.rb
CHANGED
|
@@ -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
|
-
|
|
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?
|
data/lib/svg_conform/profile.rb
CHANGED
|
@@ -114,19 +114,25 @@ module SvgConform
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def validate(document)
|
|
117
|
-
#
|
|
118
|
-
#
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
133
|
+
sax_doc.validate_with_profile(self)
|
|
128
134
|
else
|
|
129
|
-
# Fallback to DOM mode for
|
|
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
|
-
|
|
143
|
+
ValidationResult.new(document, self, context)
|
|
138
144
|
end
|
|
139
145
|
end
|
|
140
146
|
|