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
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/png,%GZ"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:text/html;base64,PHA+eDwvcD4="/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <use href="file:///etc/passwd#id"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="http://example.com/a.png"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="https://example.com/a.png"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <rect width="1" height="1" onclick="f()"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="//example.com/a.png"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <use href="other.svg#id"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="images/a.png"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <script>alert(1)</script>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxpbWFnZSBocmVmPSJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lQanhwYldGblpTQm9jbVZtUFNKa1lYUmhPbWx0WVdkbEwzTjJaeXQ0Yld3N1ltRnpaVFkwTEZCSVRqSmFlVUkwWWxkNGRXTjZNR2xoU0ZJd1kwUnZka3d6WkROa2VUVXpUWGsxZG1OdFkzWk5ha0YzVFVNNWVtUnRZMmxRYW5od1lsZEdibHBUUW05amJWWnRVRk5LYTFsWVVtaFBiV3gwV1Zka2JFd3pUakphZVhRMFlsZDNOMWx0Um5wYVZGa3dURVpDU1ZScVNtRmxWVWt3V1d4a05HUlhUalpOUjJ4b1UwWkpkMWt3VW5aa2EzZDZXa1JPYTJWVVZYcFVXR3N4WkcxT2RGa3pXazVoYTBZelZGVk5OV1Z0VW5SWk1teFJZVzVvZDFsc1pFZGliSEJVVVcwNWFtSldXblJWUms1TFlURnNXVlZ0YUZCaVYzZ3dWMVprYTJKRmQzcFVha3BoWlZoUk1GbHNaRE5PTVd4MFVtNXdZVlpHYTNkVVJWcERVMVpTY1ZOdFJteFdWV3QzVjFkNGEwNUhVbGhVYWxwT1VqSjRiMVV3V2twa01XdDNWVzVhYTJFelpEWlhhMUpQWVRKV1ZWWlljRlZYUjNONFdrY3hUMlJHYTNwWGF6Vm9ZVEJaZWxaR1ZrNU9WMVowVlc1U1drMXRlRXRWTUZwM1pERmtjMkZIZEZKTmEydzJWMjV3YzFOdFJuSlNiVFZWVmxVMVExcFZWbk5TVmtwWVlrWkdhR0p0WnpGV01uaHJWREF4Um1KRmJHRlRSVXBvVlRCYVMyUnNWa2RVYTNCc1VsZDRkMVZYTURWWlZsbDVaVWhXV2xaWGFGTlVNVlp6WTFaS1dHSkZNV3hoYkVVd1ZrVlNUMVF3TVhOalJGcFBVa1JDY0ZSSWJ6QlBSWGQ2VkdwS1lXVnFVVGxKYVRnclVFTTVlbVJ0WXlzaUx6NDhMM04yWno0PSIvPjwvc3ZnPg=="/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/svg+xml;base64,PHN2Zz48Zz48L3N2Zz4="/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48aW1hZ2UgeGxpbms6aHJlZj0iaHR0cDovL2V4YW1wbGUuY29tL2EucG5nIi8+PC9zdmc+"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="urn:example:a"/>
4
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0"?>
2
+ <?xml-stylesheet type="text/css" href="other.css"?>
3
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
4
+ <rect width="1" height="1"/>
5
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2Q=="/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/jpeg,%FF%D8%FF%E0%00%10%4A%46%49%46%00%01%01%00%00%01%00%01%00%00%FF%D9"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/png,%89%50%4E%47%0D%0A%1A%0A%00%00%00%0D%49%48%44%52%00%00%00%01%00%00%00%01%08%02%00%00%00%90%77%53%DE%00%00%00%0C%49%44%41%54%78%9C%63%F8%CF%C0%00%00%03%01%01%00%C9%FE%92%EF%00%00%00%00%49%45%4E%44%AE%42%60%82"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <defs><linearGradient id="g"/><rect id="r" width="1" height="1"/></defs><use href="#r"/><use xlink:href="#r"/><rect width="1" height="1" fill="url(#g)" style="stroke:url(#g)"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxIDEiPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48L3N2Zz4="/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="2.0" viewBox="0 0 10 10">
3
+ <image width="1" height="1" href="data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 1 1%22%3E%3Crect width=%221%22 height=%221%22/%3E%3C/svg%3E"/>
4
+ </svg>
@@ -29,7 +29,7 @@ RSpec.describe SvgConform::Commands::Check do
29
29
  options = { profile: "svg_1_2_rfc", format: "table" }
30
30
  command = described_class.new(tempfile.path, options)
31
31
 
32
- expect { command.execute }.to output(/SVG Validation Report/).to_stdout
32
+ expect { command.execute }.to output(/profile svg_1_2_rfc\nVALID|profile svg_1_2_rfc\nINVALID/).to_stdout
33
33
  .and raise_error(SystemExit)
34
34
 
35
35
  tempfile.unlink
@@ -43,7 +43,7 @@ RSpec.describe SvgConform::Commands::Check do
43
43
  options = { profile: "svg_1_2_rfc", format: "table" }
44
44
  command = described_class.new(tempfile.path, options)
45
45
 
46
- expect { command.execute }.to output(/Validation Errors/).to_stdout
46
+ expect { command.execute }.to output(/INVALID: .*\n\nerror /).to_stdout
47
47
  .and raise_error(SystemExit)
48
48
 
49
49
  tempfile.unlink
@@ -77,6 +77,26 @@ RSpec.describe SvgConform::Commands::Check do
77
77
  tempfile.unlink
78
78
  end
79
79
 
80
+ it "renders JSON from the canonical report, keeping the info diagnostic" do
81
+ fixture = File.expand_path("../../fixtures/diagnostics/external-reference-info.svg",
82
+ __dir__)
83
+ options = { profile: "svg_1_2_rfc", format: "json" }
84
+ command = described_class.new(fixture, options)
85
+
86
+ expect { command.execute }
87
+ .to output(/"infos": 1.*"requirement_id": "link_validation".*"severity": "info"|"severity": "info".*"requirement_id": "link_validation"/m)
88
+ .to_stdout.and raise_error(SystemExit) { |e| expect(e.status).to eq(0) }
89
+ end
90
+
91
+ it "renders the info diagnostic in the human (table) format" do
92
+ fixture = File.expand_path("../../fixtures/diagnostics/external-reference-info.svg",
93
+ __dir__)
94
+ command = described_class.new(fixture, { profile: "svg_1_2_rfc", format: "table" })
95
+
96
+ expect { command.execute }.to output(/info link_validation/).to_stdout
97
+ .and raise_error(SystemExit)
98
+ end
99
+
80
100
  it "handles missing files gracefully" do
81
101
  options = { profile: "svg_1_2_rfc", format: "table" }
82
102
  command = described_class.new("nonexistent.svg", options)
@@ -16,5 +16,23 @@ RSpec.describe SvgConform::Commands::Profiles do
16
16
 
17
17
  expect { command.execute }.to output(/Available SVG Profiles/).to_stdout
18
18
  end
19
+
20
+ it "lists the SVG 1.1 Full static profile (name is truncated by the table)" do
21
+ command = described_class.new({})
22
+
23
+ expect { command.execute }.to output(/svg_1_1_full_.*SVG 1.1 Full/m).to_stdout
24
+ end
25
+
26
+ it "shows the svg_1_1_full_static requirements in verbose mode" do
27
+ command = described_class.new({ verbose: true })
28
+
29
+ expect { command.execute }
30
+ .to output(/Profile: svg_1_1_full_static.*Svg11DocumentRequirement/m).to_stdout
31
+ end
32
+
33
+ it "lists the opt-in svg_2_secure_static profile" do
34
+ expect { described_class.new({}).execute }
35
+ .to output(/svg_2_secure/).to_stdout
36
+ end
19
37
  end
20
38
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe SvgConform::ConformanceReport do
6
+ let(:invalid_svg) { '<svg xmlns="http://www.w3.org/2000/svg"><rect width="5" height="5" fill="red"/></svg>' }
7
+ let(:result) { SvgConform::Validator.new.validate(invalid_svg, profile: :svg_1_2_rfc) }
8
+
9
+ describe ".from_svg_conform_result" do
10
+ it "does not include info diagnostics in the legacy channels" do
11
+ info = SvgConform::Validator.new.validate_file(
12
+ File.expand_path("../fixtures/diagnostics/external-reference-info.svg", __dir__),
13
+ profile: :svg_1_2_rfc,
14
+ )
15
+ report = described_class.from_svg_conform_result("i.svg", info)
16
+
17
+ expect(report.valid).to be(true)
18
+ expect(report.errors.total_count).to eq(0)
19
+ expect(report.warnings.total_count).to eq(0)
20
+ end
21
+
22
+ it "still accepts use_svgcheck_mapping" do
23
+ report = described_class.from_svg_conform_result("t.svg", result, profile: :svg_1_2_rfc,
24
+ use_svgcheck_mapping: true)
25
+
26
+ expect(report).to be_a(described_class)
27
+ end
28
+
29
+ context "with lens_core installed" do
30
+ before do
31
+ unless SvgConform.lens_core_available?
32
+ skip "the optional lens_core gem is not installed"
33
+ end
34
+ end
35
+
36
+ it "keeps its signature and legacy shape, projected from the canonical report" do
37
+ report = described_class.from_svg_conform_result("t.svg", result, profile: :svg_1_2_rfc)
38
+ canonical = result.to_lens_core_report
39
+
40
+ expect(report.tool).to eq("svg_conform")
41
+ expect(report.filename).to eq("t.svg")
42
+ expect(report.valid).to eq(canonical.valid)
43
+ expect(report.errors.total_count).to eq(canonical.summary["errors"])
44
+ expect(report.warnings.total_count).to eq(canonical.summary["warnings"])
45
+ expect(report.errors.issues.map(&:message))
46
+ .to eq(canonical.diagnostics.select(&:error?).map(&:message))
47
+ expect(report.errors.by_requirement.values.sum).to eq(report.errors.total_count)
48
+ expect(report.to_yaml).to include("errors:", "total_count:", "by_requirement:")
49
+ end
50
+
51
+ it "does not mutate the canonical report" do
52
+ before = result.to_lens_core_report.to_h
53
+ described_class.from_svg_conform_result("t.svg", result, profile: :svg_1_2_rfc,
54
+ use_svgcheck_mapping: true)
55
+
56
+ expect(result.to_lens_core_report.to_h).to eq(before)
57
+ end
58
+ end
59
+
60
+ context "without lens_core" do
61
+ before do
62
+ if SvgConform.lens_core_available?
63
+ skip "lens_core is installed; the direct projection is not in effect"
64
+ end
65
+ end
66
+
67
+ it "projects the validation result directly" do
68
+ report = described_class.from_svg_conform_result("t.svg", result, profile: :svg_1_2_rfc)
69
+
70
+ expect(report.tool).to eq("svg_conform")
71
+ expect(report.filename).to eq("t.svg")
72
+ expect(report.valid).to be(false)
73
+ expect(report.errors.total_count).to eq(result.errors.length)
74
+ expect(report.errors.by_requirement.values.sum).to eq(report.errors.total_count)
75
+ expect(report.errors.issues.map(&:requirement_id).sort)
76
+ .to eq(result.errors.map(&:requirement_id).sort)
77
+ expect(report.to_yaml).to include("errors:", "total_count:", "by_requirement:")
78
+ end
79
+ end
80
+ end
81
+
82
+ describe ".from_svgcheck_result" do
83
+ it "remains callable and reports valid for empty output" do
84
+ report = described_class.from_svgcheck_result("t.svg", "")
85
+
86
+ expect(report.tool).to eq("svgcheck")
87
+ expect(report.valid).to be(true)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe SvgConform::Diagnostics::LensCoreAdapter do
6
+ let(:requirement) do
7
+ SvgConform::Requirements::ViewboxRequiredRequirement.new(
8
+ id: "viewbox_required", description: "d",
9
+ citation_uri: "https://example.com/spec#viewbox",
10
+ citation_label: "Spec viewBox"
11
+ )
12
+ end
13
+ let(:profile) do
14
+ SvgConform::Profile.new(
15
+ name: "demo", specification_uri: "https://example.com/spec",
16
+ specification_label: "Demo Spec", requirements: [requirement]
17
+ )
18
+ end
19
+ let(:node) { Struct.new(:line, :column, :name).new(3, 5, "svg") }
20
+ let(:context) { SvgConform::ValidationContext.new(nil, profile) }
21
+ let(:result) { SvgConform::ValidationResult.new(nil, profile, context) }
22
+
23
+ def add_all(data: {})
24
+ context.add_error(node: node, message: "bad error", requirement_id: "viewbox_required",
25
+ data: data)
26
+ context.add_error(node: node, message: "bad validity", requirement_id: "id_reference",
27
+ severity: :validity_error)
28
+ context.add_warning(rule: Struct.new(:id).new("warn_rule"), node: node, message: "a warning")
29
+ context.add_notice(rule: nil, node: node, message: "an info", requirement_id: "some_notice")
30
+ end
31
+
32
+ describe ".call" do
33
+ it "maps all four channels to one schema-valid report in channel order" do
34
+ add_all
35
+ report = described_class.call(result, filename: "f.svg")
36
+
37
+ expect(report).to be_a(LensCore::Report)
38
+ expect(report.valid).to be(false)
39
+ expect(report.summary).to eq("errors" => 2, "warnings" => 1, "infos" => 1)
40
+ expect(report.diagnostics.map { |d| [d.severity, d.requirement_id] }).to eq(
41
+ [%w[error viewbox_required], %w[error id_reference],
42
+ %w[warning warn_rule], %w[info some_notice]],
43
+ )
44
+ expect(report.diagnostics[1].kind).to eq("validity")
45
+ expect(report.diagnostics[3].kind).to eq("notice")
46
+ expect(report.extensions["file"]).to eq("f.svg")
47
+
48
+ reparsed = LensCore::Parsers::Json.parse(LensCore::Renderers::Json.render(report))
49
+ expect(reparsed).to eq(report)
50
+ end
51
+
52
+ it "keeps validity based on errors and validity errors only" do
53
+ context.add_warning(rule: "w", node: node, message: "w")
54
+ context.add_notice(rule: nil, node: node, message: "i", requirement_id: "x")
55
+
56
+ expect(described_class.call(result).valid).to be(true)
57
+ end
58
+
59
+ it "carries location, remediation fields and explicit IDs" do
60
+ add_all
61
+ error = described_class.call(result).diagnostics.first
62
+
63
+ expect(error.location.line).to eq(3)
64
+ expect(error.location.column).to eq(5)
65
+ expect(error.remediation).to eq("Add missing viewBox attribute")
66
+ expect(error.context).to include("element" => "svg", "fixable" => false,
67
+ "remediation_type" => "add")
68
+ end
69
+
70
+ it "maps source-level line and column data to an XML location" do
71
+ context.add_error(node: nil, message: "css", requirement_id: "viewbox_required",
72
+ data: { line: 7, column: 2 })
73
+ context.add_error(node: nil, message: "bad", requirement_id: "viewbox_required",
74
+ data: { line: 0, column: "x" })
75
+ context.add_error(node: node, message: "node", requirement_id: "viewbox_required",
76
+ data: { line: 99 })
77
+ first, second, third = described_class.call(result).diagnostics
78
+
79
+ expect(first.location.to_h).to include("line" => 7, "column" => 2)
80
+ expect(second.location).to be_nil
81
+ expect(third.location.line).to eq(3)
82
+ end
83
+
84
+ it "turns profile and requirement citations into LensCore citations" do
85
+ add_all
86
+ report = described_class.call(result)
87
+
88
+ expect(report.profile.citations.map(&:to_h)).to eq(
89
+ [{ "uri" => "https://example.com/spec", "label" => "Demo Spec" }],
90
+ )
91
+ expect(report.diagnostics.first.citations.map(&:uri)).to eq(["https://example.com/spec#viewbox"])
92
+ expect(report.diagnostics[2].citations).to eq([])
93
+ end
94
+
95
+ it "only includes a timestamp when supplied" do
96
+ add_all
97
+ expect(described_class.call(result).timestamp).to be_nil
98
+ expect(described_class.call(result, timestamp: "2026-10-23T00:00:00Z").to_h["timestamp"])
99
+ .to eq("2026-10-23T00:00:00Z")
100
+ end
101
+
102
+ it "normalizes symbols in issue data to JSON primitives" do
103
+ add_all(data: { attribute: :fill, nested: [1, { ok: true }] })
104
+ data = described_class.call(result).diagnostics.first.context["data"]
105
+
106
+ expect(data).to eq("attribute" => "fill", "nested" => [1, { "ok" => true }])
107
+ end
108
+
109
+ it "fails deterministically for JSON-unsafe issue data" do
110
+ add_all(data: { thing: Object.new })
111
+
112
+ expect { described_class.call(result) }
113
+ .to raise_error(SvgConform::Diagnostics::UnsupportedContextError,
114
+ /unsupported value of type Object at data\.thing/)
115
+ end
116
+ end
117
+
118
+ describe "profile loading" do
119
+ it "keeps citations through YAML loading" do
120
+ loaded = SvgConform::Profile.from_yaml(<<~YAML)
121
+ name: yaml_demo
122
+ specification_uri: https://example.com/s
123
+ specification_label: S
124
+ requirements:
125
+ - id: viewbox_required
126
+ type: ViewboxRequiredRequirement
127
+ citation_uri: https://example.com/s#v
128
+ citation_label: V
129
+ YAML
130
+
131
+ expect(loaded.specification_uri).to eq("https://example.com/s")
132
+ expect(loaded.get_requirement("viewbox_required").citation_uri).to eq("https://example.com/s#v")
133
+ expect(loaded.get_requirement("viewbox_required").citation_label).to eq("V")
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "svg_conform.gemspec" do # rubocop:disable RSpec/DescribeClass
6
+ let(:specification) { Gem::Specification.load("svg_conform.gemspec") }
7
+
8
+ it "does not gate installation on the unpublished lens_core gem" do
9
+ # lens_core is optional: the canonical diagnostics renderer rides it
10
+ # when installed, and the plain structural serializers answer when
11
+ # not. A runtime dependency on the unpublished gem made 0.2.5
12
+ # uninstallable for every consumer.
13
+ expect(specification.runtime_dependencies.map(&:name)).not_to include("lens_core")
14
+ expect(File.read("lib/svg_conform.rb")).to include("rescue LoadError")
15
+ end
16
+
17
+ it "exposes the lens_core availability probe" do
18
+ expect(SvgConform).to respond_to(:lens_core_available?)
19
+ expect(SvgConform.lens_core_available?).to be_truthy.or be_falsey
20
+ end
21
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "svg_conform/profile_compiler"
5
+
6
+ # Path is fixed by the card; the cop expects svg11_ instead of svg_1_1_.
7
+ RSpec.describe "svg_1_1_full_static profile" do # rubocop:disable RSpec/DescribeClass
8
+ let(:profile) { SvgConform::Profiles.get("svg_1_1_full_static") }
9
+ let(:fixtures) { File.expand_path("../../fixtures/profiles/svg_1_1_full_static", __dir__) }
10
+ let(:matrix) do
11
+ {
12
+ "svg11.document.xml" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGDocuments",
13
+ "svg11.document.root" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/struct.html#SVGElement",
14
+ "svg11.document.version" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/struct.html#SVGElementVersionAttribute",
15
+ "svg11.document.grammar" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGDocuments",
16
+ "svg11.document.css2" => "https://www.w3.org/TR/2008/REC-CSS2-20080411/grammar.html",
17
+ "svg11.document.xml_stylesheet" => "https://www.w3.org/TR/1999/REC-xml-stylesheet-19990629/",
18
+ "svg11.processing.script" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGViewers",
19
+ "svg11.processing.animation" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGViewers",
20
+ "svg11.processing.interaction" => "https://www.w3.org/TR/2011/REC-SVG11-20110816/conform.html#ConformingSVGViewers",
21
+ }
22
+ end
23
+ let(:invalid_cases) do
24
+ {
25
+ "malformed-xml" => "svg11.document.xml",
26
+ "wrong-namespace" => "svg11.document.root",
27
+ "wrong-version" => "svg11.document.version",
28
+ "dtd-content-model" => "svg11.document.grammar",
29
+ "dtd-attribute" => "svg11.document.grammar",
30
+ "css-grammar" => "svg11.document.css2",
31
+ "xml-stylesheet" => "svg11.document.xml_stylesheet",
32
+ "script" => "svg11.processing.script",
33
+ "animation" => "svg11.processing.animation",
34
+ "interaction" => "svg11.processing.interaction",
35
+ }
36
+ end
37
+
38
+ def fixture(kind, name)
39
+ File.join(fixtures, kind, "#{name}.svg")
40
+ end
41
+
42
+ def sax_errors(path)
43
+ SvgConform::SaxDocument.from_file(path).validate_with_profile(profile).errors
44
+ end
45
+
46
+ def dom_errors(path)
47
+ profile.validate(SvgConform::Document.from_file(path)).errors
48
+ end
49
+
50
+ def ids(errors)
51
+ errors.map(&:requirement_id).uniq.sort - ["parse_error"]
52
+ end
53
+
54
+ it "is listed, opt-in and does not change the default profile" do
55
+ expect(SvgConform::Profiles.available_profiles).to include(:svg_1_1_full_static)
56
+ expect(profile.name).to eq("svg_1_1_full_static")
57
+ expect(profile.specification_uri)
58
+ .to eq("https://www.w3.org/TR/2011/REC-SVG11-20110816/")
59
+ expect(SvgConform::Validator.new.method(:validate).parameters)
60
+ .to include(%i[key profile])
61
+ expect(SvgConform::Validator.instance_method(:validate_file)
62
+ .parameters).to include(%i[key profile])
63
+ expect(profile.remediations.to_a).to be_empty
64
+ end
65
+
66
+ it "covers the nine stable ids with their normative citations" do
67
+ declared = profile.requirements.flat_map do |req|
68
+ req.respond_to?(:categories) ? req.categories.map { |c| [c.id, c.citation] } : [[req.id, req.citation_uri]]
69
+ end
70
+ expect(declared.to_h).to eq(matrix)
71
+ end
72
+
73
+ it "builds through the existing class map and compiler" do
74
+ expect(SvgConform::Profile::REQUIREMENTS_CLASS_MAP)
75
+ .to include("Svg11DocumentRequirement", "ProcessingFeaturesRequirement")
76
+ compiled = SvgConform::ProfileCompiler.compile("svg_1_1_full_static")
77
+ expect(compiled.requirements.map { |req| req.class.name.split("::").last }.uniq)
78
+ .to eq(%w[Svg11DocumentRequirement ProcessingFeaturesRequirement])
79
+ end
80
+
81
+ %w[minimal-full css xml-stylesheet foreign-metadata].each do |name|
82
+ it "accepts valid/#{name}.svg identically in DOM and SAX" do
83
+ path = fixture("valid", name)
84
+ expect(dom_errors(path)).to be_empty
85
+ expect(sax_errors(path)).to be_empty
86
+ end
87
+ end
88
+
89
+ it "rejects each invalid fixture with the expected id in DOM and SAX" do
90
+ invalid_cases.each do |name, id|
91
+ path = fixture("invalid", name)
92
+ sax = ids(sax_errors(path))
93
+ expect(sax).to include(id), name
94
+ if name == "malformed-xml"
95
+ # DOM cannot build a document from malformed XML at all.
96
+ expect { SvgConform::Document.from_file(path) }
97
+ .to raise_error(SvgConform::ParseError)
98
+ else
99
+ expect(ids(dom_errors(path))).to eq(sax), name
100
+ end
101
+ end
102
+ end
103
+
104
+ it "gives every error a normative citation, through issues and LensCore" do
105
+ invalid_cases.each_key do |name|
106
+ result = SvgConform::SaxDocument.from_file(fixture("invalid", name)).validate_with_profile(profile)
107
+ cited = result.errors.reject { |e| e.requirement_id == "parse_error" }
108
+ expect(cited).not_to be_empty
109
+ cited.each do |error|
110
+ expect(matrix.values).to include(error.data[:citation]), "#{name} #{error.requirement_id}"
111
+ end
112
+ report = SvgConform::Diagnostics::LensCoreAdapter.call(result)
113
+ report.diagnostics.each do |diag|
114
+ next if diag.requirement_id == "parse_error"
115
+
116
+ expect(diag.citations.map(&:uri) & matrix.values).not_to be_empty
117
+ end
118
+ end
119
+ end
120
+
121
+ it "validates a file input and a string input identically" do
122
+ path = fixture("invalid", "wrong-version")
123
+ from_string = SvgConform::SaxDocument.from_content(File.read(path))
124
+ .validate_with_profile(profile)
125
+ expect(ids(from_string.errors)).to eq(ids(sax_errors(path)))
126
+ dom_string = profile.validate(SvgConform::Document.from_content(File.read(path)))
127
+ expect(ids(dom_string.errors)).to eq(ids(from_string.errors))
128
+ end
129
+
130
+ it "does not claim Tiny or Basic and offers no remediation" do
131
+ expect(profile.description).not_to match(/tiny|basic/i)
132
+ expect(profile.remediation_count).to eq(0)
133
+ end
134
+
135
+ it "runs through the CLI check command and prints a JSON report" do
136
+ command = SvgConform::Commands::Check.new(
137
+ fixture("valid", "minimal-full"),
138
+ { profile: "svg_1_1_full_static", format: "json" },
139
+ )
140
+ expect do
141
+ expect { command.execute }.to raise_error(SystemExit) do |error|
142
+ expect(error.status).to eq(0)
143
+ end
144
+ end.to output(/"id": "svg_1_1_full_static".*"valid": true/m).to_stdout
145
+ end
146
+ end