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,1355 @@
1
+ = SvgConform Ruby API Reference
2
+ :toc: left
3
+ :toclevels: 4
4
+ :sectlinks:
5
+ :sectanchors:
6
+ :source-highlighter: rouge
7
+
8
+ == Purpose
9
+
10
+ This document provides comprehensive API documentation for the SvgConform Ruby library.
11
+
12
+ The API enables programmatic:
13
+
14
+ * SVG validation against conformance profiles
15
+ * Automatic remediation of common issues
16
+ * Custom profile creation and management
17
+ * Reference manifest generation and analysis
18
+ * Batch processing of SVG files
19
+
20
+ == Quick start
21
+
22
+ [source,ruby]
23
+ ----
24
+ require 'svg_conform'
25
+
26
+ # Validate an SVG file
27
+ validator = SvgConform::Validator.new
28
+ result = validator.validate_file('image.svg', profile: :metanorma)
29
+
30
+ if result.valid?
31
+ puts "✓ SVG is valid"
32
+ else
33
+ puts "✗ Found #{result.errors.count} errors"
34
+ result.errors.each { |e| puts " - #{e.message}" }
35
+ end
36
+ ----
37
+
38
+ == Core classes
39
+
40
+ === SvgConform::Validator
41
+
42
+ The main entry point for validation operations.
43
+
44
+ ==== Constructor
45
+
46
+ [source,ruby]
47
+ ----
48
+ SvgConform::Validator.new(options = {})
49
+ ----
50
+
51
+ **Parameters**:
52
+
53
+ * `options` (Hash) - Configuration options
54
+ ** `:fix` (Boolean) - Enable automatic fixes (default: `false`)
55
+ ** `:strict` (Boolean) - Strict validation mode (default: `false`)
56
+ ** `:mode` (Symbol) - Validation mode: `:sax`, `:dom`, or `:auto` (default: `:sax`)
57
+
58
+ **Returns**: `Validator` instance
59
+
60
+ **Example**:
61
+
62
+ [source,ruby]
63
+ ----
64
+ validator = SvgConform::Validator.new(mode: :sax)
65
+ ----
66
+
67
+ ==== Instance methods
68
+
69
+ ===== validate(input, profile:, **options)
70
+
71
+ Validate SVG content or document object.
72
+
73
+ **Parameters**:
74
+
75
+ * `input` - SVG input (String, Moxml::Document, Moxml::Element, Nokogiri::XML::Document, Nokogiri::XML::Element)
76
+ * `profile` (Symbol or Profile) - Profile to validate against
77
+ * `**options` - Additional options merged with constructor options
78
+
79
+ **Returns**: `ValidationResult`
80
+
81
+ **Supported input types**:
82
+
83
+ * `String` - Raw XML content
84
+ * `Moxml::Document` or `Moxml::Element` - Moxml DOM objects
85
+ * `Nokogiri::XML::Document` or `Nokogiri::XML::Element` - Nokogiri DOM objects
86
+ * Any object responding to `to_xml`
87
+
88
+ **Example**:
89
+
90
+ [source,ruby]
91
+ ----
92
+ # String input
93
+ result = validator.validate(svg_string, profile: :metanorma)
94
+
95
+ # Nokogiri element (common in Metanorma integration)
96
+ nokogiri_element = Nokogiri::XML(svg_string).root
97
+ result = validator.validate(nokogiri_element, profile: :metanorma)
98
+
99
+ # Moxml document
100
+ moxml_doc = Moxml.new.parse(svg_string)
101
+ result = validator.validate(moxml_doc, profile: :metanorma)
102
+ ----
103
+
104
+ ===== validate_file(file_path, profile:, **options)
105
+
106
+ Validate an SVG file.
107
+
108
+ **Parameters**:
109
+
110
+ * `file_path` (String) - Path to SVG file
111
+ * `profile` (Symbol or Profile) - Profile to validate against
112
+ * `**options` - Additional options
113
+
114
+ **Returns**: `ValidationResult`
115
+
116
+ **Raises**: `ValidationError` if file not found
117
+
118
+ **Example**:
119
+
120
+ [source,ruby]
121
+ ----
122
+ result = validator.validate_file('image.svg', profile: :svg_1_2_rfc)
123
+ ----
124
+
125
+ ===== validate_files(file_paths, profile:, **options)
126
+
127
+ Validate multiple files.
128
+
129
+ **Parameters**:
130
+
131
+ * `file_paths` (Array<String>) - Array of file paths
132
+ * `profile` (Symbol or Profile) - Profile to validate against
133
+ * `**options` - Additional options
134
+
135
+ **Returns**: `Hash<String, ValidationResult>` - Hash mapping file paths to results
136
+
137
+ **Example**:
138
+
139
+ [source,ruby]
140
+ ----
141
+ files = ['image1.svg', 'image2.svg', 'image3.svg']
142
+ results = validator.validate_files(files, profile: :metanorma)
143
+
144
+ results.each do |file, result|
145
+ puts "#{file}: #{result.valid? ? '✓' : '✗'}"
146
+ end
147
+ ----
148
+
149
+ ===== available_profiles
150
+
151
+ Get list of available profiles.
152
+
153
+ **Returns**: `Array<Symbol>` - Profile names
154
+
155
+ **Example**:
156
+
157
+ [source,ruby]
158
+ ----
159
+ profiles = validator.available_profiles
160
+ # => [:base, :svg_1_2_rfc, :metanorma, :lucid, ...]
161
+ ----
162
+
163
+ === SvgConform::Profile
164
+
165
+ Represents an SVG validation profile with requirements and remediations.
166
+
167
+ ==== Class methods
168
+
169
+ ===== load_from_file(file_path)
170
+
171
+ Load a profile from a YAML file.
172
+
173
+ **Parameters**:
174
+
175
+ * `file_path` (String) - Path to profile YAML file
176
+
177
+ **Returns**: `Profile` instance
178
+
179
+ **Example**:
180
+
181
+ [source,ruby]
182
+ ----
183
+ profile = SvgConform::Profile.load_from_file('config/profiles/custom.yml')
184
+ ----
185
+
186
+ ===== save_to_file(profile, file_path)
187
+
188
+ Save a profile to a YAML file.
189
+
190
+ **Parameters**:
191
+
192
+ * `profile` (Profile) - Profile instance to save
193
+ * `file_path` (String) - Output file path
194
+
195
+ **Example**:
196
+
197
+ [source,ruby]
198
+ ----
199
+ SvgConform::Profile.save_to_file(profile, 'output.yml')
200
+ ----
201
+
202
+ ==== Instance methods
203
+
204
+ ===== validate(document)
205
+
206
+ Validate a document against this profile.
207
+
208
+ **Parameters**:
209
+
210
+ * `document` - Document to validate (Document, SaxDocument, or object responding to `to_xml`)
211
+
212
+ **Returns**: `ValidationResult`
213
+
214
+ **Example**:
215
+
216
+ [source,ruby]
217
+ ----
218
+ profile = SvgConform::Profiles.get(:metanorma)
219
+ document = SvgConform::Document.from_file('image.svg')
220
+ result = profile.validate(document)
221
+ ----
222
+
223
+ ===== apply_remediations(document)
224
+
225
+ Apply all remediations defined in the profile.
226
+
227
+ **Parameters**:
228
+
229
+ * `document` (Document) - Document to remediate
230
+
231
+ **Returns**: `Array` - Array of changes made
232
+
233
+ **Example**:
234
+
235
+ [source,ruby]
236
+ ----
237
+ profile = SvgConform::Profiles.get(:metanorma)
238
+ document = SvgConform::Document.from_file('image.svg')
239
+
240
+ changes = profile.apply_remediations(document)
241
+ puts "Applied #{changes.length} changes"
242
+
243
+ File.write('fixed.svg', document.to_xml)
244
+ ----
245
+
246
+ ===== add_requirement(requirement)
247
+
248
+ Add a requirement to the profile.
249
+
250
+ **Parameters**:
251
+
252
+ * `requirement` (BaseRequirement) - Requirement instance
253
+
254
+ **Returns**: `self` (for chaining)
255
+
256
+ **Example**:
257
+
258
+ [source,ruby]
259
+ ----
260
+ profile = SvgConform::Profile.new
261
+ profile.add_requirement(
262
+ SvgConform::Requirements::ViewboxRequiredRequirement.new
263
+ )
264
+ ----
265
+
266
+ ===== remove_requirement(requirement_id)
267
+
268
+ Remove a requirement by ID.
269
+
270
+ **Parameters**:
271
+
272
+ * `requirement_id` (String) - Requirement identifier
273
+
274
+ **Returns**: `self` (for chaining)
275
+
276
+ ===== has_requirement?(requirement_id)
277
+
278
+ Check if profile has a specific requirement.
279
+
280
+ **Parameters**:
281
+
282
+ * `requirement_id` (String) - Requirement identifier
283
+
284
+ **Returns**: `Boolean`
285
+
286
+ ===== add_remediation(remediation)
287
+
288
+ Add a remediation to the profile.
289
+
290
+ **Parameters**:
291
+
292
+ * `remediation` (BaseRemediation) - Remediation instance
293
+
294
+ **Returns**: `self` (for chaining)
295
+
296
+ ===== remove_remediation(remediation_id)
297
+
298
+ Remove a remediation by ID.
299
+
300
+ **Parameters**:
301
+
302
+ * `remediation_id` (String) - Remediation identifier
303
+
304
+ **Returns**: `self` (for chaining)
305
+
306
+ ===== requirement_count
307
+
308
+ Get number of requirements in the profile.
309
+
310
+ **Returns**: `Integer`
311
+
312
+ ===== remediation_count
313
+
314
+ Get number of remediations in the profile.
315
+
316
+ **Returns**: `Integer`
317
+
318
+ ==== Attributes
319
+
320
+ * `name` (String) - Profile name
321
+ * `description` (String) - Profile description
322
+ * `requirements` (Array<BaseRequirement>) - Array of requirements
323
+ * `remediations` (Array<BaseRemediation>) - Array of remediations
324
+
325
+ === SvgConform::ValidationResult
326
+
327
+ Result object returned from validation operations.
328
+
329
+ ==== Instance methods
330
+
331
+ ===== valid?
332
+
333
+ Check if document is valid.
334
+
335
+ **Returns**: `Boolean`
336
+
337
+ ===== has_errors?
338
+
339
+ Check if errors were found.
340
+
341
+ **Returns**: `Boolean`
342
+
343
+ ===== has_warnings?
344
+
345
+ Check if warnings were found.
346
+
347
+ **Returns**: `Boolean`
348
+
349
+ ===== fixable?
350
+
351
+ Check if issues can be automatically fixed.
352
+
353
+ **Returns**: `Boolean`
354
+
355
+ ===== error_count
356
+
357
+ Get number of errors.
358
+
359
+ **Returns**: `Integer`
360
+
361
+ ===== warning_count
362
+
363
+ Get number of warnings.
364
+
365
+ **Returns**: `Integer`
366
+
367
+ ===== failed_requirements
368
+
369
+ Get requirements that failed validation.
370
+
371
+ **Returns**: `Array` - Array of failed requirement errors
372
+
373
+ ===== reference_manifest
374
+
375
+ Get the reference manifest.
376
+
377
+ **Returns**: `ReferenceManifest` instance
378
+
379
+ ===== available_ids
380
+
381
+ Convenience accessor for all defined IDs.
382
+
383
+ **Returns**: `Array<IdDefinition>`
384
+
385
+ ===== internal_references
386
+
387
+ Get all internal references (fragment identifiers).
388
+
389
+ **Returns**: `Array<BaseReference>`
390
+
391
+ ===== external_references
392
+
393
+ Get all external references.
394
+
395
+ **Returns**: `Array<BaseReference>`
396
+
397
+ ===== has_external_references?
398
+
399
+ Check if document has external references.
400
+
401
+ **Returns**: `Boolean`
402
+
403
+ ===== unresolved_internal_references
404
+
405
+ Get internal references that don't resolve to defined IDs.
406
+
407
+ **Returns**: `Array<BaseReference>`
408
+
409
+ ===== to_h
410
+
411
+ Convert result to hash.
412
+
413
+ **Returns**: `Hash`
414
+
415
+ ===== to_json
416
+
417
+ Convert result to JSON string.
418
+
419
+ **Returns**: `String` - JSON representation
420
+
421
+ ==== Attributes
422
+
423
+ * `document` - The validated document
424
+ * `profile` - The profile used
425
+ * `errors` (Array) - Validation errors
426
+ * `warnings` (Array) - Validation warnings
427
+ * `reference_manifest` (ReferenceManifest) - ID and reference tracking
428
+
429
+ ==== Example
430
+
431
+ [source,ruby]
432
+ ----
433
+ result = validator.validate(svg_content, profile: :metanorma)
434
+
435
+ puts "Valid: #{result.valid?}"
436
+ puts "Errors: #{result.error_count}"
437
+ puts "IDs: #{result.available_ids.map(&:id_value).join(', ')}"
438
+
439
+ if result.has_unresolved_references?
440
+ puts "Unresolved references:"
441
+ result.unresolved_internal_references.each do |ref|
442
+ puts " - #{ref.value} at line #{ref.line_number}"
443
+ end
444
+ end
445
+ ----
446
+
447
+ === SvgConform::Document
448
+
449
+ DOM wrapper for SVG documents using Moxml.
450
+
451
+ ==== Class methods
452
+
453
+ ===== new(content)
454
+
455
+ Create document from XML string.
456
+
457
+ **Parameters**:
458
+
459
+ * `content` (String) - XML content
460
+
461
+ **Returns**: `Document` instance
462
+
463
+ **Example**:
464
+
465
+ [source,ruby]
466
+ ----
467
+ document = SvgConform::Document.new(svg_string)
468
+ ----
469
+
470
+ ===== from_file(file_path)
471
+
472
+ Create document from file.
473
+
474
+ **Parameters**:
475
+
476
+ * `file_path` (String) - Path to SVG file
477
+
478
+ **Returns**: `Document` instance
479
+
480
+ **Example**:
481
+
482
+ [source,ruby]
483
+ ----
484
+ document = SvgConform::Document.from_file('image.svg')
485
+ ----
486
+
487
+ ===== from_content(content)
488
+
489
+ Alias for `new(content)`.
490
+
491
+ ===== from_node(node)
492
+
493
+ Create document from a DOM node (Nokogiri or Moxml).
494
+
495
+ **Parameters**:
496
+
497
+ * `node` - DOM node (Nokogiri::XML::Node or Moxml element)
498
+
499
+ **Returns**: `Document` instance
500
+
501
+ **Example**:
502
+
503
+ [source,ruby]
504
+ ----
505
+ nokogiri_element = Nokogiri::XML(svg_string).root
506
+ document = SvgConform::Document.from_node(nokogiri_element)
507
+ ----
508
+
509
+ ==== Instance methods
510
+
511
+ ===== root
512
+
513
+ Get the root element.
514
+
515
+ **Returns**: Moxml element
516
+
517
+ ===== to_xml
518
+
519
+ Serialize document to XML string.
520
+
521
+ **Returns**: `String` - XML content
522
+
523
+ ===== find(xpath)
524
+
525
+ Find first element matching XPath.
526
+
527
+ **Parameters**:
528
+
529
+ * `xpath` (String) - XPath expression
530
+
531
+ **Returns**: Moxml element or `nil`
532
+
533
+ ===== find_all(xpath)
534
+
535
+ Find all elements matching XPath.
536
+
537
+ **Parameters**:
538
+
539
+ * `xpath` (String) - XPath expression
540
+
541
+ **Returns**: `Array` - Array of Moxml elements
542
+
543
+ ==== Example
544
+
545
+ [source,ruby]
546
+ ----
547
+ document = SvgConform::Document.from_file('image.svg')
548
+
549
+ # Access root element
550
+ root = document.root
551
+ puts "Root: #{root.name}"
552
+
553
+ # Find elements
554
+ rects = document.find_all('//svg:rect')
555
+ puts "Found #{rects.size} rectangles"
556
+
557
+ # Modify and serialize
558
+ root.set_attribute('width', '500')
559
+ File.write('modified.svg', document.to_xml)
560
+ ----
561
+
562
+ === SvgConform::RemediationRunner
563
+
564
+ Runner for applying remediations to SVG files.
565
+
566
+ ==== Constructor
567
+
568
+ [source,ruby]
569
+ ----
570
+ SvgConform::RemediationRunner.new(profile:, options: {})
571
+ ----
572
+
573
+ **Parameters**:
574
+
575
+ * `profile` (Symbol or Profile) - Profile to use
576
+ * `options` (Hash) - Options
577
+ ** `:verbose` (Boolean) - Verbose output (default: `false`)
578
+ ** `:strict` (Boolean) - Strict mode (default: `false`)
579
+
580
+ **Returns**: `RemediationRunner` instance
581
+
582
+ ==== Instance methods
583
+
584
+ ===== run_remediation(svg_content, filename: nil)
585
+
586
+ Run remediation on SVG content.
587
+
588
+ **Parameters**:
589
+
590
+ * `svg_content` (String) - SVG XML content
591
+ * `filename` (String) - Optional filename for reporting
592
+
593
+ **Returns**: `RemediationRunResult`
594
+
595
+ ===== run_remediation_file(file_path)
596
+
597
+ Run remediation on a file.
598
+
599
+ **Parameters**:
600
+
601
+ * `file_path` (String) - Path to SVG file
602
+
603
+ **Returns**: `RemediationRunResult`
604
+
605
+ **Raises**: Error if file not found
606
+
607
+ ===== run_remediation_batch(file_paths)
608
+
609
+ Run remediation on multiple files.
610
+
611
+ **Parameters**:
612
+
613
+ * `file_paths` (Array<String>) - Array of file paths
614
+
615
+ **Returns**: `Hash<String, RemediationRunResult>` - Results by file path
616
+
617
+ ===== has_remediations?
618
+
619
+ Check if profile has remediations.
620
+
621
+ **Returns**: `Boolean`
622
+
623
+ ===== available_remediations
624
+
625
+ Get remediations from the profile.
626
+
627
+ **Returns**: `Array<BaseRemediation>`
628
+
629
+ ==== Example
630
+
631
+ [source,ruby]
632
+ ----
633
+ profile = SvgConform::Profiles.get(:metanorma)
634
+ runner = SvgConform::RemediationRunner.new(profile: profile)
635
+
636
+ result = runner.run_remediation_file('input.svg')
637
+
638
+ if result.success?
639
+ File.write('output.svg', result.remediated_content)
640
+ puts "✓ Fixed #{result.issues_fixed} issues"
641
+ else
642
+ puts "✗ Remediation failed: #{result.error.message}"
643
+ end
644
+ ----
645
+
646
+ === SvgConform::RemediationRunResult
647
+
648
+ Result of remediation operation.
649
+
650
+ ==== Instance methods
651
+
652
+ ===== success?
653
+
654
+ Check if remediation was successful (all issues fixed).
655
+
656
+ **Returns**: `Boolean`
657
+
658
+ ===== error?
659
+
660
+ Check if an error occurred.
661
+
662
+ **Returns**: `Boolean`
663
+
664
+ ===== issues_fixed
665
+
666
+ Get number of issues fixed.
667
+
668
+ **Returns**: `Integer`
669
+
670
+ ===== remediations_applied
671
+
672
+ Get number of remediations applied.
673
+
674
+ **Returns**: `Integer`
675
+
676
+ ===== content_modified?
677
+
678
+ Check if content was modified.
679
+
680
+ **Returns**: `Boolean`
681
+
682
+ ===== changes_summary
683
+
684
+ Get summary of changes.
685
+
686
+ **Returns**: `String`
687
+
688
+ ===== to_h
689
+
690
+ Convert to hash.
691
+
692
+ **Returns**: `Hash`
693
+
694
+ ==== Attributes
695
+
696
+ * `filename` (String) - Source filename
697
+ * `original_content` (String) - Original SVG content
698
+ * `remediated_content` (String) - Fixed SVG content
699
+ * `initial_validation` (ValidationResult) - Validation before fixes
700
+ * `final_validation` (ValidationResult) - Validation after fixes
701
+ * `error` - Error if occurred
702
+
703
+ == Profile management
704
+
705
+ === SvgConform::Profiles
706
+
707
+ Module for managing built-in profiles.
708
+
709
+ ==== Module methods
710
+
711
+ ===== get(profile_name)
712
+
713
+ Get a profile by name.
714
+
715
+ **Parameters**:
716
+
717
+ * `profile_name` (Symbol or String) - Profile identifier
718
+
719
+ **Returns**: `Profile` instance or `nil`
720
+
721
+ **Example**:
722
+
723
+ [source,ruby]
724
+ ----
725
+ profile = SvgConform::Profiles.get(:metanorma)
726
+ profile = SvgConform::Profiles.get('svg_1_2_rfc')
727
+ ----
728
+
729
+ ===== available_profiles
730
+
731
+ Get list of available profile names.
732
+
733
+ **Returns**: `Array<Symbol>`
734
+
735
+ **Example**:
736
+
737
+ [source,ruby]
738
+ ----
739
+ profiles = SvgConform::Profiles.available_profiles
740
+ # => [:base, :svg_1_2_rfc, :metanorma, ...]
741
+
742
+ profiles.each do |name|
743
+ profile = SvgConform::Profiles.get(name)
744
+ puts "#{name}: #{profile.description}"
745
+ end
746
+ ----
747
+
748
+ === Built-in profiles
749
+
750
+ [cols="1,3"]
751
+ |===
752
+ | Profile | Description
753
+
754
+ | `:base`
755
+ | Minimal SVG validation with basic structural checks
756
+
757
+ | `:svg_1_2_rfc`
758
+ | RFC 7996 compliance for IETF XMLRFC documents (strict)
759
+
760
+ | `:svg_1_2_rfc_with_rdf`
761
+ | RFC 7996 with RDF/Dublin Core metadata support
762
+
763
+ | `:metanorma`
764
+ | Metanorma document generation requirements
765
+
766
+ | `:lucid`
767
+ | Lucid chart export compatibility
768
+
769
+ | `:no_external_css`
770
+ | Disallow external CSS, require inline styles
771
+ |===
772
+
773
+ == Requirements
774
+
775
+ All requirement classes inherit from `SvgConform::Requirements::BaseRequirement`.
776
+
777
+ === Common requirement methods
778
+
779
+ All requirements support:
780
+
781
+ * `validate_document(document, context)` - Validate a document
782
+ * `id` - Get requirement ID
783
+ * `description` - Get requirement description
784
+
785
+ === Available requirements
786
+
787
+ ==== ViewboxRequiredRequirement
788
+
789
+ Ensures SVG has a `viewBox` attribute.
790
+
791
+ [source,ruby]
792
+ ----
793
+ requirement = SvgConform::Requirements::ViewboxRequiredRequirement.new
794
+ ----
795
+
796
+ ==== AllowedElementsRequirement
797
+
798
+ Validates elements against whitelist.
799
+
800
+ **Configuration**:
801
+
802
+ * `allowed_elements` (Array<String>) - Allowed element names
803
+
804
+ [source,ruby]
805
+ ----
806
+ requirement = SvgConform::Requirements::AllowedElementsRequirement.new(
807
+ config: {
808
+ 'allowed_elements' => ['svg', 'rect', 'circle', 'path']
809
+ }
810
+ )
811
+ ----
812
+
813
+ ==== ColorRestrictionsRequirement
814
+
815
+ Enforces color format restrictions.
816
+
817
+ **Configuration**:
818
+
819
+ * `allowed_formats` (Array<String>) - e.g., `['hex', 'rgb']`
820
+
821
+ ==== FontFamilyRequirement
822
+
823
+ Restricts font families.
824
+
825
+ **Configuration**:
826
+
827
+ * `allowed_families` (Array<String>) - Allowed font names
828
+
829
+ [source,ruby]
830
+ ----
831
+ requirement = SvgConform::Requirements::FontFamilyRequirement.new(
832
+ config: {
833
+ 'allowed_families' => ['Arial', 'Helvetica', 'sans-serif']
834
+ }
835
+ )
836
+ ----
837
+
838
+ ==== NoExternalImagesRequirement
839
+
840
+ Disallows external image references.
841
+
842
+ ==== NoExternalFontsRequirement
843
+
844
+ Disallows external font references.
845
+
846
+ ==== NoExternalCssRequirement
847
+
848
+ Disallows external CSS stylesheets.
849
+
850
+ ==== NamespaceRequirement
851
+
852
+ Validates namespace declarations.
853
+
854
+ **Configuration**:
855
+
856
+ * `allowed_namespaces` (Array<String>) - Allowed namespace URIs
857
+
858
+ ==== IdReferenceRequirement
859
+
860
+ Validates that all ID references resolve.
861
+
862
+ ==== InvalidIdReferencesRequirement
863
+
864
+ Detects invalid or malformed ID references.
865
+
866
+ ==== LinkValidationRequirement
867
+
868
+ Validates hyperlink targets.
869
+
870
+ ==== StyleRequirement
871
+
872
+ Validates style attributes and values.
873
+
874
+ ==== StylePromotionRequirement
875
+
876
+ Checks for presentational attributes that should be styles.
877
+
878
+ ==== ForbiddenContentRequirement
879
+
880
+ Detects forbidden elements or attributes.
881
+
882
+ **Configuration**:
883
+
884
+ * `forbidden_elements` (Array<String>) - Forbidden element names
885
+ * `forbidden_attributes` (Array<String>) - Forbidden attribute names
886
+
887
+ == Remediations
888
+
889
+ All remediation classes inherit from `SvgConform::Remediations::BaseRemediation`.
890
+
891
+ === Common remediation methods
892
+
893
+ All remediations support:
894
+
895
+ * `apply(document, context)` - Apply remediation
896
+ * `should_execute?(failed_requirements)` - Check if should run
897
+ * `id` - Get remediation ID
898
+
899
+ === Available remediations
900
+
901
+ ==== ViewboxRemediation
902
+
903
+ Adds missing `viewBox` attribute (calculated from dimensions).
904
+
905
+ ==== ColorRemediation
906
+
907
+ Converts colors to allowed formats.
908
+
909
+ ==== FontRemediation
910
+
911
+ Replaces disallowed fonts with alternatives.
912
+
913
+ ==== FontEmbeddingRemediation
914
+
915
+ Embeds external fonts as data URIs or inline.
916
+
917
+ ==== ImageEmbeddingRemediation
918
+
919
+ Embeds external images as data URIs.
920
+
921
+ ==== NoExternalCssRemediation
922
+
923
+ Inlines external CSS stylesheets.
924
+
925
+ ==== NamespaceRemediation
926
+
927
+ Fixes namespace declarations.
928
+
929
+ ==== NamespaceAttributeRemediation
930
+
931
+ Removes unwanted namespace attributes.
932
+
933
+ ==== InvalidIdReferencesRemediation
934
+
935
+ Fixes or removes invalid ID references.
936
+
937
+ ==== StylePromotionRemediation
938
+
939
+ Converts presentation attributes to inline styles.
940
+
941
+ == Reference tracking
942
+
943
+ === SvgConform::References::ReferenceManifest
944
+
945
+ Tracks all IDs and references in an SVG document.
946
+
947
+ ==== Instance methods
948
+
949
+ ===== available_ids
950
+
951
+ Get all defined IDs.
952
+
953
+ **Returns**: `Array<IdDefinition>`
954
+
955
+ ===== internal_references
956
+
957
+ Get all internal references (fragments).
958
+
959
+ **Returns**: `Array<BaseReference>`
960
+
961
+ ===== external_references
962
+
963
+ Get all external references.
964
+
965
+ **Returns**: `Array<BaseReference>`
966
+
967
+ ===== unresolved_internal_references
968
+
969
+ Get references that don't resolve to defined IDs.
970
+
971
+ **Returns**: `Array<BaseReference>`
972
+
973
+ ===== statistics
974
+
975
+ Get reference statistics.
976
+
977
+ **Returns**: `Hash` with counts
978
+
979
+ ===== to_h
980
+
981
+ Convert to hash.
982
+
983
+ **Returns**: `Hash`
984
+
985
+ ==== Example
986
+
987
+ [source,ruby]
988
+ ----
989
+ result = validator.validate(svg_content, profile: :metanorma)
990
+ manifest = result.reference_manifest
991
+
992
+ puts "IDs defined: #{manifest.available_ids.size}"
993
+ manifest.available_ids.each do |id|
994
+ puts " - #{id.id_value} (#{id.element_name})"
995
+ end
996
+
997
+ puts "Internal refs: #{manifest.internal_references.size}"
998
+ puts "External refs: #{manifest.external_references.size}"
999
+
1000
+ if manifest.unresolved_internal_references.any?
1001
+ puts "Unresolved:"
1002
+ manifest.unresolved_internal_references.each do |ref|
1003
+ puts " - #{ref.value} at line #{ref.line_number}"
1004
+ end
1005
+ end
1006
+ ----
1007
+
1008
+ === SvgConform::References::IdDefinition
1009
+
1010
+ Represents an ID definition.
1011
+
1012
+ ==== Attributes
1013
+
1014
+ * `id_value` (String) - The ID value
1015
+ * `element_name` (String) - Element containing the ID
1016
+ * `line_number` (Integer) - Line number in source
1017
+ * `namespace` (String) - Element namespace
1018
+
1019
+ === SvgConform::References::BaseReference
1020
+
1021
+ Represents a reference to an ID.
1022
+
1023
+ ==== Attributes
1024
+
1025
+ * `value` (String) - Reference value (may include fragment)
1026
+ * `reference_type` (String) - Type: 'url', 'href', 'fill', 'fragment'
1027
+ * `line_number` (Integer) - Line number in source
1028
+ * `element_name` (String) - Element containing reference
1029
+ * `attribute_name` (String) - Attribute name
1030
+
1031
+ ==== Methods
1032
+
1033
+ * `external?` - Check if reference is external
1034
+ * `internal?` - Check if reference is internal (fragment)
1035
+ * `fragment` - Get fragment identifier (without `#`)
1036
+
1037
+ == Advanced usage
1038
+
1039
+ === Custom profile creation
1040
+
1041
+ ==== From scratch
1042
+
1043
+ [source,ruby]
1044
+ ----
1045
+ profile = SvgConform::Profile.new
1046
+ profile.name = 'custom'
1047
+ profile.description = 'Custom validation profile'
1048
+
1049
+ # Add requirements
1050
+ profile.add_requirement(
1051
+ SvgConform::Requirements::ViewboxRequiredRequirement.new
1052
+ )
1053
+ profile.add_requirement(
1054
+ SvgConform::Requirements::NoExternalImagesRequirement.new
1055
+ )
1056
+
1057
+ # Add remediations
1058
+ profile.add_remediation(
1059
+ SvgConform::Remediations::ViewboxRemediation.new
1060
+ )
1061
+ profile.add_remediation(
1062
+ SvgConform::Remediations::ImageEmbeddingRemediation.new
1063
+ )
1064
+
1065
+ # Save to file
1066
+ SvgConform::Profile.save_to_file(profile, 'custom-profile.yml')
1067
+ ----
1068
+
1069
+ ==== From YAML
1070
+
1071
+ [source,yaml]
1072
+ ----
1073
+ # custom-profile.yml
1074
+ name: custom
1075
+ description: Custom validation profile
1076
+
1077
+ requirements:
1078
+ - type: viewbox_required
1079
+ - type: no_external_images
1080
+ - type: font_family
1081
+ config:
1082
+ allowed_families:
1083
+ - Arial
1084
+ - Helvetica
1085
+
1086
+ remediations:
1087
+ - type: viewbox
1088
+ - type: image_embedding
1089
+ ----
1090
+
1091
+ [source,ruby]
1092
+ ----
1093
+ profile = SvgConform::Profile.load_from_file('custom-profile.yml')
1094
+ ----
1095
+
1096
+ === Batch processing pattern
1097
+
1098
+ [source,ruby]
1099
+ ----
1100
+ validator = SvgConform::Validator.new
1101
+ results = {}
1102
+ errors = []
1103
+
1104
+ Dir.glob('images/**/*.svg') do |file|
1105
+ begin
1106
+ result = validator.validate_file(file, profile: :metanorma)
1107
+ results[file] = result
1108
+
1109
+ unless result.valid?
1110
+ errors << { file: file, errors: result.errors }
1111
+ end
1112
+ rescue => e
1113
+ errors << { file: file, error: e.message }
1114
+ end
1115
+ end
1116
+
1117
+ # Summary
1118
+ valid_count = results.count { |_, r| r.valid? }
1119
+ puts "#{valid_count}/#{results.size} files valid"
1120
+
1121
+ # Detailed errors
1122
+ if errors.any?
1123
+ puts "\nFiles with errors:"
1124
+ errors.each do |item|
1125
+ puts "#{item[:file]}:"
1126
+ if item[:errors]
1127
+ item[:errors].each { |e| puts " - #{e.message}" }
1128
+ else
1129
+ puts " - #{item[:error]}"
1130
+ end
1131
+ end
1132
+ end
1133
+ ----
1134
+
1135
+ === Remediation workflow
1136
+
1137
+ [source,ruby]
1138
+ ----
1139
+ profile = SvgConform::Profiles.get(:metanorma)
1140
+ runner = SvgConform::RemediationRunner.new(profile: profile)
1141
+
1142
+ Dir.glob('input/**/*.svg').each do |input_file|
1143
+ output_file = input_file.sub('input/', 'output/')
1144
+
1145
+ # Ensure output directory exists
1146
+ FileUtils.mkdir_p(File.dirname(output_file))
1147
+
1148
+ # Run remediation
1149
+ result = runner.run_remediation_file(input_file)
1150
+
1151
+ if result.success?
1152
+ File.write(output_file, result.remediated_content)
1153
+ puts "✓ #{input_file} → #{output_file} (#{result.issues_fixed} fixed)"
1154
+ else
1155
+ puts "✗ #{input_file}: #{result.error.message}"
1156
+ end
1157
+ end
1158
+ ----
1159
+
1160
+ === Reference manifest analysis
1161
+
1162
+ [source,ruby]
1163
+ ----
1164
+ result = validator.validate(svg_content, profile: :metanorma)
1165
+ manifest = result.reference_manifest
1166
+
1167
+ # Export manifest
1168
+ File.write('manifest.json', manifest.to_json)
1169
+ File.write('manifest.yaml', manifest.to_yaml)
1170
+
1171
+ # Analyze ID usage
1172
+ manifest.available_ids.each do |id_def|
1173
+ # Find references to this ID
1174
+ refs = manifest.internal_references.select do |ref|
1175
+ ref.fragment == id_def.id_value
1176
+ end
1177
+
1178
+ puts "ID '#{id_def.id_value}' referenced #{refs.size} times"
1179
+ end
1180
+
1181
+ # Check for unused IDs
1182
+ manifest.available_ids.each do |id_def|
1183
+ referenced = manifest.internal_references.any? do |ref|
1184
+ ref.fragment == id_def.id_value
1185
+ end
1186
+
1187
+ puts "Unused ID: #{id_def.id_value}" unless referenced
1188
+ end
1189
+ ----
1190
+
1191
+ === Integration with Metanorma
1192
+
1193
+ [source,ruby]
1194
+ ----
1195
+ # Metanorma integration example
1196
+ class SVGProcessor
1197
+ def initialize
1198
+ @validator = SvgConform::Validator.new
1199
+ @profile = SvgConform::Profiles.get(:metanorma)
1200
+ @runner = SvgConform::RemediationRunner.new(profile: @profile)
1201
+ end
1202
+
1203
+ def process_svg_element(nokogiri_element)
1204
+ # Validate without serializing by user
1205
+ result = @validator.validate(nokogiri_element, profile: :metanorma)
1206
+
1207
+ if result.valid?
1208
+ return { status: :valid, element: nokogiri_element }
1209
+ end
1210
+
1211
+ # Apply fixes
1212
+ remediation = @runner.run_remediation(nokogiri_element.to_xml)
1213
+
1214
+ if remediation.success?
1215
+ # Parse fixed content and return new element
1216
+ fixed_doc = Nokogiri::XML(remediation.remediated_content)
1217
+ return {
1218
+ status: :fixed,
1219
+ element: fixed_doc.root,
1220
+ issues_fixed: remediation.issues_fixed
1221
+ }
1222
+ else
1223
+ return {
1224
+ status: :error,
1225
+ errors: result.errors,
1226
+ element: nokogiri_element
1227
+ }
1228
+ end
1229
+ end
1230
+ end
1231
+ ----
1232
+
1233
+ == Error handling
1234
+
1235
+ === Common errors
1236
+
1237
+ ==== ValidationError
1238
+
1239
+ Raised when validation fails due to invalid input.
1240
+
1241
+ [source,ruby]
1242
+ ----
1243
+ begin
1244
+ result = validator.validate_file('missing.svg', profile: :metanorma)
1245
+ rescue SvgConform::ValidationError => e
1246
+ puts "Validation error: #{e.message}"
1247
+ end
1248
+ ----
1249
+
1250
+ ==== ProfileError
1251
+
1252
+ Raised when profile is not found or invalid.
1253
+
1254
+ [source,ruby]
1255
+ ----
1256
+ begin
1257
+ profile = SvgConform::Profiles.get(:nonexistent)
1258
+ rescue SvgConform::ProfileError => e
1259
+ puts "Profile error: #{e.message}"
1260
+ end
1261
+ ----
1262
+
1263
+ === Best practices
1264
+
1265
+ [source,ruby]
1266
+ ----
1267
+ def safe_validate(file_path, profile)
1268
+ validator = SvgConform::Validator.new
1269
+
1270
+ # Check file exists
1271
+ unless File.exist?(file_path)
1272
+ return { error: "File not found: #{file_path}" }
1273
+ end
1274
+
1275
+ # Validate
1276
+ begin
1277
+ result = validator.validate_file(file_path, profile: profile)
1278
+ {
1279
+ valid: result.valid?,
1280
+ errors: result.errors.map(&:message),
1281
+ warnings: result.warnings.map(&:message)
1282
+ }
1283
+ rescue SvgConform::ValidationError => e
1284
+ { error: "Validation failed: #{e.message}" }
1285
+ rescue => e
1286
+ { error: "Unexpected error: #{e.message}" }
1287
+ end
1288
+ end
1289
+ ----
1290
+
1291
+ == Performance considerations
1292
+
1293
+ === SAX vs DOM modes
1294
+
1295
+ **SAX mode** (default):
1296
+
1297
+ * Memory-efficient, constant memory usage
1298
+ * Handles any file size (tested up to 100MB+)
1299
+ * Slightly slower for small files (<100KB)
1300
+ * Recommended for production
1301
+
1302
+ **DOM mode**:
1303
+
1304
+ * Fast for small files
1305
+ * Can hang or crash on large files (>1MB)
1306
+ * Required for remediations
1307
+ * Use only for small files
1308
+
1309
+ [source,ruby]
1310
+ ----
1311
+ # SAX mode (recommended for validation only)
1312
+ validator = SvgConform::Validator.new(mode: :sax)
1313
+
1314
+ # Auto mode (SAX for files >1MB, DOM otherwise)
1315
+ validator = SvgConform::Validator.new(mode: :auto)
1316
+
1317
+ # DOM mode (only for small files)
1318
+ validator = SvgConform::Validator.new(mode: :dom)
1319
+ ----
1320
+
1321
+ === Batch processing optimization
1322
+
1323
+ [source,ruby]
1324
+ ----
1325
+ # Efficient batch processing
1326
+ validator = SvgConform::Validator.new(mode: :sax)
1327
+ profile = SvgConform::Profiles.get(:metanorma) # Cache profile
1328
+
1329
+ files.each do |file|
1330
+ result = validator.validate_file(file, profile: profile)
1331
+ # Process result...
1332
+ end
1333
+ ----
1334
+
1335
+ == Related documentation
1336
+
1337
+ * link:../README.adoc[Main README]
1338
+ * link:cli_guide.adoc[CLI Guide]
1339
+ * link:profiles.adoc[Profile Documentation]
1340
+ * link:requirements.adoc[Requirements Reference]
1341
+ * link:remediation.adoc[Remediation Reference]
1342
+ * link:reference_manifest.adoc[Reference Manifest Documentation]
1343
+
1344
+ == Support
1345
+
1346
+ For issues or questions:
1347
+
1348
+ * GitHub: https://github.com/metanorma/svg_conform
1349
+ * Issues: https://github.com/metanorma/svg_conform/issues
1350
+
1351
+ == Conclusion
1352
+
1353
+ The SvgConform API provides a comprehensive toolkit for SVG validation and remediation. The object-oriented design makes it easy to extend with custom requirements and remediations, while the built-in profiles handle common use cases.
1354
+
1355
+ For CLI usage, see the link:cli_guide.adoc[CLI Guide].