svg_conform 0.2.4 → 0.2.6

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +1 -0
  3. data/.github/workflows/rake.yml +3 -0
  4. data/Gemfile +7 -3
  5. data/Gemfile.lock +272 -0
  6. data/TODO.issue-100/00-overview.md +135 -0
  7. data/TODO.issue-100/01-build-lens-core-v1.md +92 -0
  8. data/TODO.issue-100/02-adopt-lens-core-in-svg.md +123 -0
  9. data/TODO.issue-100/03-ship-svg-1-1-full-static.md +138 -0
  10. data/TODO.issue-100/04-ship-svg-2-secure-static.md +131 -0
  11. data/TODO.issue-100/05-freeze-candidate-and-handoff.md +108 -0
  12. data/TODO.issue-100/90-progress.md +65 -0
  13. data/config/profiles/metanorma.yml +3 -3
  14. data/config/profiles/svg_1_1_full_static.yml +69 -0
  15. data/config/profiles/svg_2_secure_static.yml +55 -0
  16. data/config/requirements/processing_features_static.yml +29 -0
  17. data/data/svg_1_1/manifest.yml +7 -0
  18. data/data/svg_1_1/svg11-flat-20110816.dtd +5664 -0
  19. data/docs/api_reference.adoc +19 -1
  20. data/docs/cli_guide.adoc +35 -45
  21. data/docs/diagnostics.adoc +73 -0
  22. data/docs/lens_core_handoff.adoc +84 -0
  23. data/docs/profiles.adoc +94 -1
  24. data/lib/svg_conform/cli.rb +3 -3
  25. data/lib/svg_conform/commands/check.rb +11 -60
  26. data/lib/svg_conform/conformance_report.rb +79 -1
  27. data/lib/svg_conform/diagnostics/lens_core_adapter.rb +200 -0
  28. data/lib/svg_conform/document.rb +12 -1
  29. data/lib/svg_conform/node_helpers.rb +1 -1
  30. data/lib/svg_conform/profile.rb +11 -2
  31. data/lib/svg_conform/quality_metrics/complexity_calculator.rb +3 -3
  32. data/lib/svg_conform/references/base_reference.rb +153 -0
  33. data/lib/svg_conform/report_comparator.rb +4 -4
  34. data/lib/svg_conform/requirements/base_requirement.rb +6 -0
  35. data/lib/svg_conform/requirements/external_references_requirement.rb +234 -0
  36. data/lib/svg_conform/requirements/link_validation_requirement.rb +2 -0
  37. data/lib/svg_conform/requirements/processing_features_requirement.rb +175 -0
  38. data/lib/svg_conform/requirements/svg_1_1_document_requirement.rb +213 -0
  39. data/lib/svg_conform/requirements.rb +6 -0
  40. data/lib/svg_conform/sax_document.rb +8 -4
  41. data/lib/svg_conform/sax_validation_handler.rb +22 -1
  42. data/lib/svg_conform/validation/css2_core_grammar.rb +359 -0
  43. data/lib/svg_conform/validation/css_references.rb +122 -0
  44. data/lib/svg_conform/validation/error_tracker.rb +3 -1
  45. data/lib/svg_conform/validation/svg_1_1_document_conformance.rb +155 -0
  46. data/lib/svg_conform/validation_context.rb +25 -5
  47. data/lib/svg_conform/validation_result.rb +91 -16
  48. data/lib/svg_conform/version.rb +1 -1
  49. data/lib/svg_conform.rb +24 -0
  50. data/spec/fixtures/diagnostics/external-reference-info.svg +6 -0
  51. data/spec/fixtures/plantuml/README.adoc +11 -0
  52. data/spec/fixtures/plantuml/unit-type.svg +1 -0
  53. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/animation.svg +4 -0
  54. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/css-grammar.svg +4 -0
  55. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/dtd-attribute.svg +4 -0
  56. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/dtd-content-model.svg +4 -0
  57. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/interaction.svg +4 -0
  58. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/malformed-xml.svg +4 -0
  59. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/script.svg +5 -0
  60. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/wrong-namespace.svg +4 -0
  61. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/wrong-version.svg +4 -0
  62. data/spec/fixtures/profiles/svg_1_1_full_static/invalid/xml-stylesheet.svg +5 -0
  63. data/spec/fixtures/profiles/svg_1_1_full_static/valid/css.svg +5 -0
  64. data/spec/fixtures/profiles/svg_1_1_full_static/valid/foreign-metadata.svg +6 -0
  65. data/spec/fixtures/profiles/svg_1_1_full_static/valid/minimal-full.svg +5 -0
  66. data/spec/fixtures/profiles/svg_1_1_full_static/valid/xml-stylesheet.svg +5 -0
  67. data/spec/fixtures/profiles/svg_2_secure_static/invalid/animation.svg +4 -0
  68. data/spec/fixtures/profiles/svg_2_secure_static/invalid/css-import.svg +4 -0
  69. data/spec/fixtures/profiles/svg_2_secure_static/invalid/css-url.svg +4 -0
  70. data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-bad-png.svg +4 -0
  71. data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-empty-png.svg +4 -0
  72. data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-malformed-base64.svg +4 -0
  73. data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-malformed-percent.svg +4 -0
  74. data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-unsupported-type.svg +4 -0
  75. data/spec/fixtures/profiles/svg_2_secure_static/invalid/file-like-fragment.svg +4 -0
  76. data/spec/fixtures/profiles/svg_2_secure_static/invalid/http.svg +4 -0
  77. data/spec/fixtures/profiles/svg_2_secure_static/invalid/https.svg +4 -0
  78. data/spec/fixtures/profiles/svg_2_secure_static/invalid/interaction.svg +4 -0
  79. data/spec/fixtures/profiles/svg_2_secure_static/invalid/protocol-relative.svg +4 -0
  80. data/spec/fixtures/profiles/svg_2_secure_static/invalid/relative-file-fragment.svg +4 -0
  81. data/spec/fixtures/profiles/svg_2_secure_static/invalid/relative-file.svg +4 -0
  82. data/spec/fixtures/profiles/svg_2_secure_static/invalid/script.svg +4 -0
  83. data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-excessive-nesting.svg +4 -0
  84. data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-malformed-xml.svg +4 -0
  85. data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-nested-external.svg +4 -0
  86. data/spec/fixtures/profiles/svg_2_secure_static/invalid/urn.svg +4 -0
  87. data/spec/fixtures/profiles/svg_2_secure_static/invalid/xml-stylesheet.svg +5 -0
  88. data/spec/fixtures/profiles/svg_2_secure_static/valid/jpeg-base64.svg +4 -0
  89. data/spec/fixtures/profiles/svg_2_secure_static/valid/jpeg-percent.svg +4 -0
  90. data/spec/fixtures/profiles/svg_2_secure_static/valid/png-base64.svg +4 -0
  91. data/spec/fixtures/profiles/svg_2_secure_static/valid/png-percent.svg +4 -0
  92. data/spec/fixtures/profiles/svg_2_secure_static/valid/same-document-fragment.svg +4 -0
  93. data/spec/fixtures/profiles/svg_2_secure_static/valid/svg-base64.svg +4 -0
  94. data/spec/fixtures/profiles/svg_2_secure_static/valid/svg-percent.svg +4 -0
  95. data/spec/svg_conform/commands/check_command_spec.rb +22 -2
  96. data/spec/svg_conform/commands/profiles_command_spec.rb +18 -0
  97. data/spec/svg_conform/conformance_report_spec.rb +90 -0
  98. data/spec/svg_conform/diagnostics/lens_core_adapter_spec.rb +136 -0
  99. data/spec/svg_conform/gemspec_spec.rb +21 -0
  100. data/spec/svg_conform/profiles/svg_1_1_full_static_profile_spec.rb +146 -0
  101. data/spec/svg_conform/profiles/svg_2_secure_static_profile_spec.rb +150 -0
  102. data/spec/svg_conform/profiles_plantuml_spec.rb +81 -0
  103. data/spec/svg_conform/references/data_uri_reference_spec.rb +97 -0
  104. data/spec/svg_conform/references/integration_spec.rb +30 -0
  105. data/spec/svg_conform/references/reference_classifier_spec.rb +17 -0
  106. data/spec/svg_conform/requirements/external_references_requirement_spec.rb +109 -0
  107. data/spec/svg_conform/requirements/link_validation_requirement_spec.rb +16 -0
  108. data/spec/svg_conform/requirements/processing_features_requirement_spec.rb +75 -0
  109. data/spec/svg_conform/requirements/svg_1_1_document_requirement_spec.rb +141 -0
  110. data/spec/svg_conform/validation/css2_core_grammar_spec.rb +116 -0
  111. data/spec/svg_conform/validation/svg_1_1_document_conformance_spec.rb +144 -0
  112. data/spec/svg_conform/validation_context_source_plumbing_spec.rb +122 -0
  113. data/spec/svg_conform/validation_context_spec.rb +39 -0
  114. data/spec/svg_conform/validation_result_spec.rb +123 -0
  115. data/svg_conform.gemspec +3 -2
  116. metadata +102 -6
@@ -336,10 +336,28 @@ Check if document is valid.
336
336
 
337
337
  ===== has_errors?
338
338
 
339
- Check if errors were found.
339
+ Check if errors or validity errors were found (the same condition as
340
+ `valid?` being false).
340
341
 
341
342
  **Returns**: `Boolean`
342
343
 
344
+ ===== infos / info_count
345
+
346
+ Informational diagnostics (for example external reference notices) and their
347
+ count. Infos never affect `valid?`.
348
+
349
+ ===== to_lens_core_report(timestamp: nil)
350
+
351
+ Returns the canonical `LensCore::Report` for this result. Errors and validity
352
+ errors are both severity `error`. The timestamp is included only when the
353
+ caller supplies one. See xref:diagnostics.adoc[Diagnostics].
354
+
355
+ ===== to_h / to_json / to_s
356
+
357
+ Project the canonical report. `to_h` is symbol-keyed. `file`,
358
+ `fixes_applied`, `fixable` and `reference_manifest` are retained as
359
+ compatibility extensions.
360
+
343
361
  ===== has_warnings?
344
362
 
345
363
  Check if warnings were found.
data/docs/cli_guide.adoc CHANGED
@@ -125,6 +125,18 @@ Available profiles:
125
125
  * `metanorma` - Metanorma requirements
126
126
  * `lucid` - Lucid profile
127
127
  * `no_external_css` - No external CSS
128
+ * `svg_1_1_full_static` - SVG 1.1 Full document grammar, static processing (see link:profiles.adoc#svg-1-1-full-static-profile[profiles])
129
+ * `svg_2_secure_static` - SVG 2 secure-static resource rules (opt-in; see link:profiles.adoc#svg-2-secure-static-profile[profiles])
130
+
131
+ Examples:
132
+
133
+ * `svg_conform check drawing.svg --profile svg_1_1_full_static`
134
+ * SVG 2 secure-static:
135
+
136
+ [source,sh]
137
+ ----
138
+ svg_conform check drawing.svg --profile svg_2_secure_static --format json
139
+ ----
128
140
 
129
141
  ==== Output format (single file)
130
142
 
@@ -133,9 +145,9 @@ Output format for single file mode (default: `table`)
133
145
  +
134
146
  Options:
135
147
  +
136
- * `table` - Human-readable table (default)
137
- * `json` - JSON format
138
- * `yaml` - YAML format
148
+ * `table` - Human-readable text (default); an alias for the LensCore human renderer
149
+ * `json` - LensCore diagnostics JSON (schema version 1)
150
+ * `yaml` - LensCore diagnostics YAML (schema version 1)
139
151
 
140
152
  `--output=FILE` or `-o`::
141
153
  Save report to file instead of stdout
@@ -403,67 +415,45 @@ fi
403
415
 
404
416
  ==== Table format (default)
405
417
 
406
- Human-readable table with error summary:
418
+ Plain text rendered by LensCore from the canonical report. Errors, warnings
419
+ and infos all appear; see xref:diagnostics.adoc[Diagnostics].
407
420
 
408
421
  [source,text]
409
422
  ----
410
- SVG Validation Report
411
- ==================================================
412
- File: file.svg
413
- Profile: metanorma
414
- Valid: ✗
415
- Total Errors: 2
416
-
417
- Validation Errors:
423
+ svg_conform 0.2.4, profile metanorma
424
+ INVALID: 1 error, 0 warnings, 1 info
418
425
 
419
- • Viewbox is required
420
- at svg
426
+ error viewbox_required (viewbox_violation)
427
+ at /svg[1]
428
+ Viewbox is required
429
+ fix: Add missing viewBox attribute
421
430
 
422
- • External images are not allowed
423
- at image
431
+ info link_validation (reference_violation)
432
+ at /svg[1]/a[1]
433
+ External reference: https://example.com/page
424
434
  ----
425
435
 
426
436
  ==== JSON format
427
437
 
428
- Machine-readable JSON using lutaml-model:
438
+ The LensCore diagnostics report, schema version 1 (see
439
+ xref:diagnostics.adoc[Diagnostics]). No timestamp is emitted by the CLI.
429
440
 
430
441
  [source,json]
431
442
  ----
432
443
  {
433
- "filename": "file.svg",
434
- "profile": "metanorma",
444
+ "schema_version": 1,
445
+ "producer": { "name": "svg_conform", "version": "0.2.4" },
446
+ "profile": { "id": "metanorma", "citations": [] },
435
447
  "valid": false,
436
- "errors": {
437
- "total_count": 2,
438
- "issues": [
439
- {
440
- "requirement_id": "viewbox_required",
441
- "message": "Viewbox is required",
442
- "element": "svg",
443
- "line_number": 1
444
- }
445
- ]
446
- }
448
+ "summary": { "errors": 1, "warnings": 0, "infos": 1 },
449
+ "diagnostics": [ ... ],
450
+ "file": "file.svg"
447
451
  }
448
452
  ----
449
453
 
450
454
  ==== YAML format
451
455
 
452
- Machine-readable YAML using lutaml-model:
453
-
454
- [source,yaml]
455
- ----
456
- filename: file.svg
457
- profile: metanorma
458
- valid: false
459
- errors:
460
- total_count: 2
461
- issues:
462
- - requirement_id: viewbox_required
463
- message: Viewbox is required
464
- element: svg
465
- line_number: 1
466
- ----
456
+ The same report and key order as the JSON format, rendered as YAML.
467
457
 
468
458
  ==== Batch report format
469
459
 
@@ -0,0 +1,73 @@
1
+ = SvgConform Diagnostics
2
+ :toc: left
3
+ :source-highlighter: rouge
4
+
5
+ == Purpose
6
+
7
+ SvgConform reports one validation result as one
8
+ https://github.com/claricle/lens_core[LensCore] diagnostics report (schema
9
+ version 1). JSON, YAML and human output all render from that report.
10
+
11
+ == Producing a report
12
+
13
+ [source,ruby]
14
+ ----
15
+ result = SvgConform::Validator.new.validate_file("a.svg", profile: :svg_1_2_rfc)
16
+
17
+ report = result.to_lens_core_report # LensCore::Report
18
+ report = result.to_lens_core_report(timestamp: Time.now) # timestamp only if given
19
+
20
+ LensCore::Renderers::Json.render(report)
21
+ LensCore::Renderers::Yaml.render(report)
22
+ LensCore::Renderers::Human.render(report)
23
+ ----
24
+
25
+ `SvgConform::Diagnostics::LensCoreAdapter.call(result, filename: nil,
26
+ timestamp: nil)` is the single entry point behind these.
27
+
28
+ == Mapping
29
+
30
+ [cols="2,1,3"]
31
+ |===
32
+ | svg_conform source | Severity | Kind
33
+
34
+ | `errors` | `error` | the issue's violation type, else `validation`
35
+ | `validity_errors` | `error` | `validity`
36
+ | `warnings` | `warning` | the issue's violation type, else `validation`
37
+ | `infos` | `info` | the issue's violation type, else `notice`
38
+ |===
39
+
40
+ An `unknown_violation` type counts as absent. Order within each channel is
41
+ preserved and channels are emitted in the order above. Requirement IDs are
42
+ the explicit IDs on the issues; infos created through
43
+ `ValidationContext#add_notice(requirement_id:)` and
44
+ `#add_external_reference_notice(requirement_id:)` carry their requirement's
45
+ ID. `valid` is true only when there are no errors and no validity errors.
46
+
47
+ Each diagnostic carries `location` (XML line, column and element path when
48
+ known), `remediation` (the suggested action when remediable) and `context`
49
+ (`element`, remediation flags, and the issue `data`). Issue data must be
50
+ JSON-safe; symbols become strings, and any other object raises
51
+ `SvgConform::Diagnostics::UnsupportedContextError`.
52
+
53
+ == Citations
54
+
55
+ `Profile` accepts `specification_uri` and `specification_label`; every
56
+ requirement accepts `citation_uri` and `citation_label`. Both are read from
57
+ profile YAML and become LensCore citations on the report profile and on
58
+ diagnostics for that requirement. Existing profiles may omit them.
59
+
60
+ == Transition
61
+
62
+ For one transition the previous surfaces remain callable:
63
+
64
+ * `ValidationResult#to_h`, `#to_json`, `#to_s(format:)` project the canonical
65
+ report, with `file`, `fixes_applied`, `fixable` and `reference_manifest` as
66
+ compatibility extensions.
67
+ * `ConformanceReport.from_svg_conform_result` (deprecated) builds the
68
+ canonical report first and projects its errors and warnings into the legacy
69
+ YAML shape. `use_svgcheck_mapping:` and `from_svgcheck_result` keep their
70
+ signatures; svgcheck filtering happens after canonical construction and
71
+ never mutates the report.
72
+ * `svg_conform check --format table|json|yaml` renders through LensCore;
73
+ `table` is an alias for the human renderer.
@@ -0,0 +1,84 @@
1
+ = LensCore v1 freeze candidate: adoption handoff
2
+ :toc: left
3
+ :source-highlighter: rouge
4
+
5
+ == Purpose
6
+
7
+ This page lets another lens adopt https://github.com/claricle/lens_core[LensCore]
8
+ without reading any SVG code. It names the exact immutable commits and digests.
9
+ Nothing here is tagged, released or published.
10
+
11
+ == Pinned facts
12
+
13
+ [cols="2,5"]
14
+ |===
15
+ | LensCore freeze commit | `d8903de61dec5b600853e1d605a712cc83f99c41` (claricle/lens_core#4 merge commit)
16
+ | Schema path | `schemas/lens-diagnostics-v1.json`
17
+ | Schema SHA-256 | `90f0976166aaa88417d3eec59cab8288e085b89230a9521e431169acb3e87cba`
18
+ | Schema version | `1` (`LensCore::SCHEMA_VERSION`)
19
+ | Golden manifest | `spec/fixtures/v1/golden/manifest.yml`
20
+ | Contract commit used to render the goldens | `98c21c82bc97e3c8c4038048acdcec4a177c5f7c` (schema and `lib/` are identical at the freeze commit)
21
+ | svg_conform producer commit named by the manifest | `5bf0f80af2a80bb5fd9f8a68cc95e470ae43e5d0`
22
+ | svg_conform pin | `Gemfile` and `Gemfile.lock` resolve LensCore to the freeze commit above
23
+ |===
24
+
25
+ == Adopting
26
+
27
+ Pin the exact commit, never a branch:
28
+
29
+ [source,ruby]
30
+ ----
31
+ gem "lens_core", git: "https://github.com/claricle/lens_core.git",
32
+ ref: "d8903de61dec5b600853e1d605a712cc83f99c41"
33
+ ----
34
+
35
+ Canonical calls:
36
+
37
+ [source,ruby]
38
+ ----
39
+ report = LensCore::Parsers::Json.parse(File.binread("report.json")) # or Parsers::Yaml
40
+ LensCore::Renderers::Json.render(report) # canonical bytes
41
+ LensCore::Renderers::Yaml.render(report)
42
+ LensCore::Renderers::Human.render(report)
43
+ ----
44
+
45
+ Rendering a parsed canonical golden returns the input bytes exactly. Timestamps
46
+ are omitted unless the producer passes one.
47
+
48
+ == Compatibility and errors
49
+
50
+ * A `schema_version` other than `1` raises `LensCore::UnsupportedVersionError`;
51
+ a non-integer one raises `LensCore::SchemaError`.
52
+ * Within version 1 consumers accept new `kind` values and unknown members.
53
+ Removing a member, changing its meaning, or adding a `severity` or location
54
+ `type` is a new schema version.
55
+ * Other failures are `LensCore::ParseError`, `UnsafeYamlError`,
56
+ `InvalidEncodingError` and `SchemaError` (with the failing path).
57
+ * `summary` is derived and checked on parse; `valid: true` with an error is
58
+ rejected. The full rules are in LensCore `docs/diagnostics-v1.adoc`.
59
+
60
+ == Evidence completed
61
+
62
+ * Three real svg_conform exports in `spec/fixtures/v1/golden/svg_conform/`:
63
+ SVG 1.1 invalid CSS grammar, SVG 2 external reference, and a four-channel
64
+ report (error, validity error, warning, info) built through the public
65
+ `SvgConform::Diagnostics::LensCoreAdapter`.
66
+ * Each golden validates against the schema, parses, and renders byte for byte;
67
+ summary counts equal the diagnostics.
68
+ * `spec/fixtures/host_app/` reads the whole golden directory using LensCore
69
+ alone (no svg_conform, no other lens).
70
+ * `script/refresh_svg_goldens PATH_TO_SVG_CONFORM` regenerates the three SVG
71
+ goldens from a checkout at the manifest's producer commit.
72
+
73
+ == Not evidence
74
+
75
+ `spec/fixtures/v1/golden/examples/binary-location.json` is a format-neutral
76
+ contract example of the binary location form. It is not PNG, EMF or PostScript
77
+ implementation evidence; no lens other than svg_conform has adopted LensCore.
78
+
79
+ == Future work, outside this issue
80
+
81
+ * EMF, PNG and PostScript adapters.
82
+ * Tagging and publishing a LensCore gem, and replacing every Git pin with a
83
+ released constraint.
84
+ * Migrating other lenses.
data/docs/profiles.adoc CHANGED
@@ -17,7 +17,46 @@ Each profile consists of:
17
17
 
18
18
  == Available profiles
19
19
 
20
- SvgConform includes 5 built-in profiles, each designed for different use cases and compliance standards.
20
+ SvgConform includes built-in profiles, each designed for different use cases and compliance standards.
21
+
22
+ [[svg-1-1-full-static-profile]]
23
+ === SVG 1.1 Full static profile (`:svg_1_1_full_static`)
24
+
25
+ **Purpose**: Opt-in check that a document is a conforming SVG 1.1 (Second Edition) Full document under static processing. It validates the Recommendation's document grammar; it is not an element allow-list.
26
+
27
+ **Specification**: https://www.w3.org/TR/2011/REC-SVG11-20110816/[SVG 1.1 Second Edition]
28
+
29
+ **Requirements** (each reports under its own ID and carries its normative citation in issues and LensCore reports):
30
+
31
+ [cols="2,4,3"]
32
+ |===
33
+ | ID | Rule | Citation
34
+
35
+ | `svg11.document.xml` | Well-formed XML; the original declaration, doctype and processing instructions are provable from the source | https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGDocuments[Conforming SVG Documents]
36
+ | `svg11.document.root` | Document element is `svg` in `http://www.w3.org/2000/svg` | https://www.w3.org/TR/2011/REC-SVG11-20110816/struct.html#SVGElement[The svg element]
37
+ | `svg11.document.version` | Root declares `version="1.1"` | https://www.w3.org/TR/2011/REC-SVG11-20110816/struct.html#SVGElementVersionAttribute[version attribute]
38
+ | `svg11.document.grammar` | Elements, attributes and content models validate against the vendored flat DTD (`data/svg_1_1/svg11-flat-20110816.dtd`) after the Appendix G.2 foreign-namespace transform, applied to a copy | https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGDocuments[Conforming SVG Documents]
39
+ | `svg11.document.css2` | `style` attributes and `style` elements parse under the CSS 2 core grammar (syntax only) | https://www.w3.org/TR/2008/REC-CSS2-20080411/grammar.html[CSS 2 grammar]
40
+ | `svg11.document.xml_stylesheet` | `xml-stylesheet` processing instructions have valid pseudo-attributes, an `href`, and a CSS `type` | https://www.w3.org/TR/1999/REC-xml-stylesheet-19990629/[xml-stylesheet]
41
+ | `svg11.processing.script` | No `script` element or script event-handler attribute | https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGViewers[Conforming SVG Viewers]
42
+ | `svg11.processing.animation` | No animation element or animation-timing attribute | same
43
+ | `svg11.processing.interaction` | No interaction-only event attribute, `cursor` element or `cursor` attribute | same
44
+ |===
45
+
46
+ **Example**:
47
+
48
+ [source,shell]
49
+ ----
50
+ svg_conform check drawing.svg --profile svg_1_1_full_static --format json
51
+ ----
52
+
53
+ **Behavior notes**:
54
+
55
+ * Validation is offline: the DTD is resolved from the gem, and any external entity declaration is an error. No file is read and no network request is made.
56
+ * DOM and SAX validation give the same result for the same input. A malformed document cannot be loaded as a DOM, so DOM mode raises `SvgConform::ParseError` where SAX reports `svg11.document.xml`.
57
+ * Declaration, doctype and processing-instruction checks need the original text. A document validated from an already-parsed node (re-serialized source) is reported under `svg11.document.xml` because those claims cannot be proved.
58
+
59
+ **Non-goals**: SVG Tiny or Basic, rendering or browser behavior, CSS property and value semantics, external style sheets referenced by `xml-stylesheet`, and remediation (the profile has none).
21
60
 
22
61
  [[svg-1-2-rfc-profile]]
23
62
  === SVG 1.2 RFC profile (`:svg_1_2_rfc`)
@@ -184,6 +223,51 @@ remediations:
184
223
 
185
224
  See link:remediation.adoc#viewbox-remediation[ViewboxRemediation] and link:remediation.adoc#namespace-remediation[NamespaceRemediation] for detailed remediation information.
186
225
 
226
+ [[svg-2-secure-static-profile]]
227
+ === SVG 2 secure-static profile (`:svg_2_secure_static`)
228
+
229
+ **Purpose**: Checks the resource and processing rules of the secure-static processing mode in the SVG 2 Candidate Recommendation of 6 October 2026 (`https://www.w3.org/TR/2026/CR-SVG2-20261006/conform.html#secure-static-mode`).
230
+
231
+ The profile is opt-in. No existing profile and no default selection changes.
232
+
233
+ **Stable requirement IDs** (all cite the dated secure-static section):
234
+
235
+ [cols="1,3"]
236
+ |===
237
+ |ID |Rule
238
+
239
+ |`svg2.secure_static.script` |Rejects `script` elements and script event-handler attributes.
240
+ |`svg2.secure_static.animation` |Rejects SVG animation elements and timing attributes, and CSS `animation*` / `transition*` attributes.
241
+ |`svg2.secure_static.interaction` |Rejects interaction event attributes and the `cursor` attribute or element.
242
+ |`svg2.secure_static.external_reference` |Rejects references that need another document, a local file or the network.
243
+ |===
244
+
245
+ The first three IDs configure the shared `ProcessingFeaturesRequirement`; the fourth is `ExternalReferencesRequirement`, which classifies every reference with `SvgConform::References::ReferenceClassifier`.
246
+
247
+ **Accepted references**:
248
+
249
+ [cols="2,1,3"]
250
+ |===
251
+ |Reference |Result |Notes
252
+
253
+ |`#fragment` in the current document |allowed |`link_validation` still reports a missing target.
254
+ |`data:image/png` or `data:image/jpeg`, base64 or percent-encoded |allowed |Payload must be non-empty and a structurally valid PNG (chunk checksums) or JPEG (SOI/EOI).
255
+ |`data:image/svg+xml`, base64 or percent-encoded |allowed |Must decode to one well-formed SVG document with no external reference of its own; nesting is limited to 3 levels.
256
+ |Other media types, malformed base64 or percent-encoding |rejected |
257
+ |`http:`, `https:`, `//host/...`, `urn:`, `file:` and other schemes |rejected |
258
+ |Relative paths, including `other.svg#id` |rejected |
259
+ |===
260
+
261
+ Inspected locations: `href` and `xlink:href`; `fill`, `stroke`, `filter`, `clip-path`, `mask`, `marker-*` and `cursor` attributes; CSS `url(...)` and `@import` in `style` attributes and `style` elements; and `xml-stylesheet` processing instructions in the original source.
262
+
263
+ **Non-goals**: This profile does not claim complete SVG 2 document, rendering or CSS conformance. It does not check `font-face` sources, CSS `@font-face`, script or animation expressed only inside CSS rules, or the contents of raster images.
264
+
265
+ **Example**:
266
+ [source,sh]
267
+ ----
268
+ svg_conform check drawing.svg --profile svg_2_secure_static
269
+ ----
270
+
187
271
  [[no-external-css-profile]]
188
272
  === No external CSS profile (`:no_external_css`)
189
273
 
@@ -274,6 +358,15 @@ See link:remediation.adoc#namespace-attribute-remediation[NamespaceAttributeReme
274
358
  * **Structural compliance**: Proper namespaces and viewBox required
275
359
  * **No external dependencies**: External CSS and fonts strictly prohibited
276
360
 
361
+ **PlantUML attributes**:
362
+
363
+ The Metanorma compatibility profile accepts `contentStyleType` and `zoomAndPan`
364
+ on `svg`, `textLength` and `lengthAdjust` on `text`, and `title` on `a`.
365
+ These explicit allowances preserve generated diagrams' measured text layout and
366
+ link titles. They do not change the strict RFC 7996 profiles or permit arbitrary
367
+ additional attributes. Fragment links into a surrounding document must also be
368
+ validated by that document's build.
369
+
277
370
  **Event Handler Support**:
278
371
 
279
372
  The metanorma profile allows SVG event handler attributes (e.g., `onmouseover`, `onmouseout`, `onfocus`, `onblur`) for interactive elements. This is configured using wildcard attribute patterns:
@@ -45,9 +45,9 @@ module SvgConform
45
45
  - Directory: Requires --output-dir or --in-place
46
46
 
47
47
  REPORT FORMATS:
48
- - table: Human-readable (default for single file)
49
- - json: JSON format using lutaml-model
50
- - yaml: YAML format using lutaml-model
48
+ - table: Human-readable text (default for single file; alias for the LensCore human renderer)
49
+ - json: LensCore diagnostics JSON (schema version 1)
50
+ - yaml: LensCore diagnostics YAML (schema version 1)
51
51
 
52
52
  EXAMPLES:
53
53
  svg_conform check file.svg -p metanorma
@@ -69,78 +69,29 @@ module SvgConform
69
69
  result = validator.validate_file(@file,
70
70
  profile: @options[:profile].to_sym)
71
71
 
72
- # Generate report
73
- report = SvgConform::ConformanceReport.from_svg_conform_result(
74
- File.basename(@file),
75
- result,
76
- profile: @options[:profile].to_sym,
72
+ # One canonical report; every output format renders from it.
73
+ report = SvgConform::Diagnostics::LensCoreAdapter.call(
74
+ result, filename: File.basename(@file)
77
75
  )
78
76
 
79
- # Output based on format
80
- case @options[:format]
81
- when "yaml"
82
- output_yaml(report)
83
- when "json"
84
- output_json(report)
85
- else
86
- output_table(report)
87
- end
77
+ write_output(render(report))
88
78
 
89
79
  # Create remediated file if requested
90
80
  create_remediated_file(result) if @options[:fix]
91
81
 
92
82
  # Exit with appropriate code based on validation result
93
- exit(report.valid? ? 0 : 1)
83
+ exit(report.valid ? 0 : 1)
94
84
  end
95
85
 
96
- def output_table(report)
97
- puts Paint["SVG Validation Report", :bold]
98
- puts "=" * 50
99
- puts "File: #{@file}"
100
- puts "Profile: #{@options[:profile]}"
101
- puts "Valid: #{report.valid? ? Paint['✓', :green] : Paint['✗', :red]}"
102
- puts "Total Errors: #{report.errors.total_count}"
103
- puts
104
-
105
- if report.errors.total_count.positive?
106
- puts Paint["Validation Errors:", :bold]
107
- puts
108
-
109
- # Group errors by type
110
- error_groups = report.errors.issues.group_by(&:message)
111
-
112
- error_groups.each do |message, errors|
113
- puts Paint["• #{message}", :red]
114
- if errors.size > 1
115
- puts Paint[" (#{errors.size} occurrences)",
116
- :black]
117
- end
118
-
119
- # Show first few locations
120
- errors.first(3).each do |error|
121
- puts Paint[" at #{error.element}", :black] if error.element
122
- end
123
-
124
- if errors.size > 3
125
- puts Paint[" ... and #{errors.size - 3} more",
126
- :black]
127
- end
128
- puts
129
- end
130
- else
131
- puts Paint["✓ No validation errors found", :green]
86
+ # "table" stays an accepted alias for the human renderer.
87
+ def render(report)
88
+ case @options[:format]
89
+ when "yaml" then LensCore::Renderers::Yaml.render(report)
90
+ when "json" then LensCore::Renderers::Json.render(report)
91
+ else LensCore::Renderers::Human.render(report)
132
92
  end
133
93
  end
134
94
 
135
- def output_yaml(report)
136
- output = report.to_yaml
137
- write_output(output)
138
- end
139
-
140
- def output_json(report)
141
- write_output(report.to_json)
142
- end
143
-
144
95
  def write_output(content)
145
96
  if @options[:output]
146
97
  File.write(@options[:output], content)
@@ -74,13 +74,20 @@ module SvgConform
74
74
  # Create report from SvgConform ValidationResult
75
75
  def self.from_svg_conform_result(filename, validation_result, profile: nil,
76
76
  use_svgcheck_mapping: false)
77
+ # The canonical LensCore report is built first, for every path; the
78
+ # legacy shape below is a deprecated projection of it. Without the
79
+ # optional lens_core gem the report projects the validation result
80
+ # directly.
81
+ canonical = if SvgConform.lens_core_available?
82
+ validation_result.to_lens_core_report
83
+ end
77
84
  report = new
78
85
  report.filename = filename
79
86
  report.profile = profile
80
87
  report.tool = "svg_conform"
81
88
  report.version = SvgConform::VERSION
82
89
  report.timestamp = Time.now.iso8601
83
- report.valid = validation_result.valid?
90
+ report.valid = canonical ? canonical.valid : validation_result.valid?
84
91
  report.errors = IssueSummary.new
85
92
  report.errors.total_count = 0
86
93
  report.errors.by_requirement = {}
@@ -104,6 +111,15 @@ use_svgcheck_mapping: false)
104
111
  end
105
112
  end
106
113
 
114
+ unless use_svgcheck_mapping
115
+ if canonical
116
+ project_canonical(report, canonical)
117
+ else
118
+ project_validation_result(report, validation_result)
119
+ end
120
+ return report
121
+ end
122
+
107
123
  # Process errors (including conditional validity_errors for svgcheck compatibility)
108
124
  all_errors = validation_result.errors.dup
109
125
 
@@ -180,6 +196,68 @@ use_svgcheck_mapping: false)
180
196
  report
181
197
  end
182
198
 
199
+ # Fills the legacy summaries from the canonical report's error and
200
+ # warning diagnostics (infos have no legacy channel).
201
+ def self.project_canonical(report, canonical)
202
+ {
203
+ "error" => report.errors, "warning" => report.warnings
204
+ }.each do |severity, summary|
205
+ diagnostics = canonical.diagnostics.select do |d|
206
+ d.severity == severity
207
+ end
208
+ summary.total_count = diagnostics.length
209
+ diagnostics.group_by(&:requirement_id).each do |id, list|
210
+ summary.by_requirement[id] = list.length
211
+ end
212
+ summary.issues = diagnostics.map { |d| issue_from(d) }
213
+ end
214
+ end
215
+
216
+ def self.issue_from(diagnostic)
217
+ data = diagnostic.context["data"]
218
+ data = {} unless data.is_a?(Hash)
219
+ issue = ConformanceIssue.new
220
+ issue.type = diagnostic.severity
221
+ issue.requirement_id = diagnostic.requirement_id
222
+ issue.message = diagnostic.message
223
+ issue.element = diagnostic.context["element"]
224
+ if diagnostic.severity == "error"
225
+ issue.attribute = data["attribute"]
226
+ issue.value = data["value"]&.to_s
227
+ end
228
+ issue.line = diagnostic.location&.line
229
+ issue
230
+ end
231
+
232
+ # The lens_core-free projection: fill the summaries straight from the
233
+ # validation result, mirroring project_canonical's output shape.
234
+ def self.project_validation_result(report, result)
235
+ {
236
+ "error" => [report.errors, result.errors],
237
+ "warning" => [report.warnings, result.warnings],
238
+ }.each do |severity, (summary, issues)|
239
+ summary.total_count = issues.length
240
+ issues.group_by(&:requirement_id).each do |id, list|
241
+ summary.by_requirement[id] = list.length
242
+ end
243
+ summary.issues = issues.map do |error|
244
+ issue = ConformanceIssue.new
245
+ issue.type = severity
246
+ issue.requirement_id = error.requirement_id
247
+ issue.message = error.message
248
+ issue.element = error.element_name
249
+ if severity == "error"
250
+ issue.attribute = error.data[:attribute] if error.respond_to?(:data) && error.data
251
+ issue.value = error.data[:value]&.to_s if error.respond_to?(:data) && error.data
252
+ end
253
+ issue.line = error.line if error.respond_to?(:line)
254
+ issue
255
+ end
256
+ end
257
+ end
258
+ private_class_method :project_canonical, :issue_from,
259
+ :project_validation_result
260
+
183
261
  # Create report from svgcheck error output
184
262
  def self.from_svgcheck_result(filename, error_content,
185
263
  _output_content = nil)