svg_conform 0.2.4 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/.github/workflows/rake.yml +4 -1
- data/Gemfile +6 -3
- data/Gemfile.lock +211 -0
- data/TODO.issue-100/00-overview.md +135 -0
- data/TODO.issue-100/01-build-lens-core-v1.md +92 -0
- data/TODO.issue-100/02-adopt-lens-core-in-svg.md +123 -0
- data/TODO.issue-100/03-ship-svg-1-1-full-static.md +138 -0
- data/TODO.issue-100/04-ship-svg-2-secure-static.md +131 -0
- data/TODO.issue-100/05-freeze-candidate-and-handoff.md +108 -0
- data/TODO.issue-100/90-progress.md +65 -0
- data/config/profiles/metanorma.yml +4 -4
- data/config/profiles/svg_1_1_full_static.yml +69 -0
- data/config/profiles/svg_2_secure_static.yml +55 -0
- data/config/requirements/processing_features_static.yml +29 -0
- data/data/svg_1_1/manifest.yml +7 -0
- data/data/svg_1_1/svg11-flat-20110816.dtd +5664 -0
- data/docs/api_reference.adoc +19 -1
- data/docs/cli_guide.adoc +35 -45
- data/docs/diagnostics.adoc +73 -0
- data/docs/lens_core_handoff.adoc +84 -0
- data/docs/profiles.adoc +94 -1
- data/lib/svg_conform/cli.rb +3 -4
- data/lib/svg_conform/commands/check.rb +11 -60
- data/lib/svg_conform/conformance_report.rb +79 -1
- data/lib/svg_conform/diagnostics/lens_core_adapter.rb +200 -0
- data/lib/svg_conform/document.rb +12 -1
- data/lib/svg_conform/external_checkers/svgcheck/parser.rb +1 -3
- data/lib/svg_conform/node_helpers.rb +1 -1
- data/lib/svg_conform/profile.rb +11 -2
- data/lib/svg_conform/quality_metrics/complexity_calculator.rb +3 -3
- data/lib/svg_conform/quality_metrics/configuration.rb +4 -2
- data/lib/svg_conform/references/base_reference.rb +153 -0
- data/lib/svg_conform/references/reference_manifest.rb +1 -2
- data/lib/svg_conform/report_comparator.rb +4 -4
- data/lib/svg_conform/requirements/base_requirement.rb +6 -0
- data/lib/svg_conform/requirements/external_references_requirement.rb +234 -0
- data/lib/svg_conform/requirements/link_validation_requirement.rb +2 -0
- data/lib/svg_conform/requirements/processing_features_requirement.rb +175 -0
- data/lib/svg_conform/requirements/svg_1_1_document_requirement.rb +213 -0
- data/lib/svg_conform/requirements.rb +6 -0
- data/lib/svg_conform/sax_document.rb +8 -4
- data/lib/svg_conform/sax_validation_handler.rb +22 -1
- data/lib/svg_conform/validation/css2_core_grammar.rb +359 -0
- data/lib/svg_conform/validation/css_references.rb +122 -0
- data/lib/svg_conform/validation/error_tracker.rb +40 -9
- data/lib/svg_conform/validation/svg_1_1_document_conformance.rb +154 -0
- data/lib/svg_conform/validation_context.rb +25 -5
- data/lib/svg_conform/validation_result.rb +91 -16
- data/lib/svg_conform/version.rb +1 -1
- data/lib/svg_conform/yaml_io.rb +20 -0
- data/lib/svg_conform.rb +25 -0
- data/spec/fixtures/diagnostics/external-reference-info.svg +6 -0
- data/spec/fixtures/plantuml/README.adoc +11 -0
- data/spec/fixtures/plantuml/unit-type.svg +1 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/animation.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/css-grammar.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/dtd-attribute.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/dtd-content-model.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/interaction.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/malformed-xml.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/script.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/wrong-namespace.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/wrong-version.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/xml-stylesheet.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/css.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/foreign-metadata.svg +6 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/minimal-full.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/xml-stylesheet.svg +5 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/animation.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/css-import.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/css-url.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-bad-png.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-empty-png.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-malformed-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-malformed-percent.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-unsupported-type.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/file-like-fragment.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/http.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/https.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/interaction.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/protocol-relative.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/relative-file-fragment.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/relative-file.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/script.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-excessive-nesting.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-malformed-xml.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-nested-external.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/urn.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/xml-stylesheet.svg +5 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/jpeg-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/jpeg-percent.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/png-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/png-percent.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/same-document-fragment.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/svg-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/svg-percent.svg +4 -0
- data/spec/fixtures/quality_metrics/aliases.yml +7 -0
- data/spec/svg_conform/commands/check_command_spec.rb +22 -2
- data/spec/svg_conform/commands/profiles_command_spec.rb +18 -0
- data/spec/svg_conform/conformance_report_spec.rb +90 -0
- data/spec/svg_conform/diagnostics/lens_core_adapter_spec.rb +136 -0
- data/spec/svg_conform/gemspec_spec.rb +34 -0
- data/spec/svg_conform/profiles/metanorma_profile_spec.rb +48 -0
- data/spec/svg_conform/profiles/svg_1_1_full_static_profile_spec.rb +146 -0
- data/spec/svg_conform/profiles/svg_2_secure_static_profile_spec.rb +150 -0
- data/spec/svg_conform/profiles_plantuml_spec.rb +81 -0
- data/spec/svg_conform/quality_metrics/configuration_spec.rb +82 -0
- data/spec/svg_conform/references/data_uri_reference_spec.rb +97 -0
- data/spec/svg_conform/references/integration_spec.rb +30 -0
- data/spec/svg_conform/references/reference_classifier_spec.rb +17 -0
- data/spec/svg_conform/requirements/external_references_requirement_spec.rb +109 -0
- data/spec/svg_conform/requirements/link_validation_requirement_spec.rb +16 -0
- data/spec/svg_conform/requirements/processing_features_requirement_spec.rb +75 -0
- data/spec/svg_conform/requirements/svg_1_1_document_requirement_spec.rb +141 -0
- data/spec/svg_conform/requirements/text_as_path_requirement_spec.rb +9 -5
- data/spec/svg_conform/validation/css2_core_grammar_spec.rb +116 -0
- data/spec/svg_conform/validation/svg_1_1_document_conformance_spec.rb +144 -0
- data/spec/svg_conform/validation_context_source_plumbing_spec.rb +122 -0
- data/spec/svg_conform/validation_context_spec.rb +39 -0
- data/spec/svg_conform/validation_result_spec.rb +123 -0
- data/spec/svg_conform/yaml_io_spec.rb +41 -0
- data/svg_conform.gemspec +4 -2
- metadata +120 -6
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
RSpec.describe SvgConform::Requirements::ExternalReferencesRequirement do
|
|
7
|
+
let(:requirement) do
|
|
8
|
+
described_class.new(id: "ext", citation_uri: "https://example.test/cite")
|
|
9
|
+
end
|
|
10
|
+
let(:profile) { SvgConform::Profile.new(name: "x", requirements: [requirement]) }
|
|
11
|
+
let(:ns) { 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' }
|
|
12
|
+
|
|
13
|
+
def svg(body, pre: "")
|
|
14
|
+
%(#{pre}<svg #{ns} viewBox="0 0 1 1">#{body}</svg>)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dom(xml)
|
|
18
|
+
profile.validate(SvgConform::Document.from_content(xml))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def sax(xml)
|
|
22
|
+
SvgConform::SaxDocument.from_content(xml).validate_with_profile(profile)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def b64_svg(inner)
|
|
26
|
+
"data:image/svg+xml;base64,#{Base64.strict_encode64(inner)}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
"same-document href" => ['<use href="#a"/>', 0],
|
|
31
|
+
"same-document xlink:href" => ['<use xlink:href="#a"/>', 0],
|
|
32
|
+
"http href" => ['<image href="http://e.test/a.png"/>', 1],
|
|
33
|
+
"xlink http href" => ['<image xlink:href="https://e.test/a.png"/>', 1],
|
|
34
|
+
"anchor to web" => ['<a href="https://e.test/">x</a>', 1],
|
|
35
|
+
"presentation url" => ['<rect fill="url(other.svg#g)"/>', 1],
|
|
36
|
+
"presentation fragment" => ['<rect fill="url(#g)"/>', 0],
|
|
37
|
+
"style attribute url" => ['<rect style="fill:url("x.svg#g")"/>', 1],
|
|
38
|
+
"style element import string" => ['<style>@import "a.css";</style>', 1],
|
|
39
|
+
"style element import url counted once" => ["<style>@import url(a.css);</style>", 1],
|
|
40
|
+
"style element url" => ["<style>a{fill:url(a.svg#x)}</style>", 1],
|
|
41
|
+
"css comment ignored" => ["<style>/* url(a.svg) */ a{fill:red}</style>", 0],
|
|
42
|
+
"escaped url function" => ['<rect style="fill:u\\72l(https://e.test/x.png)"/>', 1],
|
|
43
|
+
"escaped url function uppercase hex" => ['<rect style="fill:\\55RL(https://e.test/x.png)"/>', 1],
|
|
44
|
+
"escaped url in presentation attribute" => ['<rect fill="u\\72l(https://e.test/x.png)"/>', 1],
|
|
45
|
+
"escaped url with quoted string" => ['<rect style="fill:u\\72l(\'https://e.test/x.png\')"/>', 1],
|
|
46
|
+
"escaped characters inside the url" => ['<rect style="fill:url(\\68ttps://e.test/x.png)"/>', 1],
|
|
47
|
+
"escaped import string" => ['<style>@\\69mport "a.css";</style>', 1],
|
|
48
|
+
"escaped import url" => ['<style>@imp\\6frt url(a.css);</style>', 1],
|
|
49
|
+
"escaped import with escaped url" => ['<style>@\\69mport u\\72l(a.css);</style>', 1],
|
|
50
|
+
"url text inside a css string is not a reference" => ['<style>a{content:"url(a.svg)"}</style>', 0],
|
|
51
|
+
"escaped url with same-document fragment" => ['<rect style="fill:u\\72l(#g)"/>', 0],
|
|
52
|
+
"javascript scheme" => ['<a href="javascript:x()">x</a>', 1],
|
|
53
|
+
}.each do |label, (body, count)|
|
|
54
|
+
it "#{label}: #{count} finding(s) through DOM and SAX" do
|
|
55
|
+
xml = svg(body)
|
|
56
|
+
expect(dom(xml).errors.size).to eq(count)
|
|
57
|
+
expect(sax(xml).errors.map(&:message)).to eq(dom(xml).errors.map(&:message))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "reports xml-stylesheet processing instructions from the source" do
|
|
62
|
+
xml = svg("", pre: %(<?xml-stylesheet href="a.css"?>))
|
|
63
|
+
expect(dom(xml).errors.size).to eq(1)
|
|
64
|
+
expect(sax(xml).errors.size).to eq(1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "uses the requirement id and citation in error data" do
|
|
68
|
+
error = sax(svg('<image href="//e.test/a.png"/>')).errors.first
|
|
69
|
+
expect(error.requirement_id).to eq("ext")
|
|
70
|
+
expect(error.data).to include(citation: "https://example.test/cite",
|
|
71
|
+
reference_type: "RelativePathReference")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "nested SVG data URLs" do
|
|
75
|
+
let(:plain) { '<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>' }
|
|
76
|
+
|
|
77
|
+
it "accepts a self-contained SVG" do
|
|
78
|
+
expect(sax(svg(%(<image href="#{b64_svg(plain)}"/>))).errors).to be_empty
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "rejects malformed XML, non-svg roots and external entities" do
|
|
82
|
+
["<svg><g></svg>", "<html/>",
|
|
83
|
+
%(<!DOCTYPE svg SYSTEM "http://e.test/x.dtd"><svg/>)].each do |bad|
|
|
84
|
+
errors = sax(svg(%(<image href="#{b64_svg(bad)}"/>))).errors
|
|
85
|
+
expect(errors.size).to eq(1), bad
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "reports nested external content with a JSON-safe reason" do
|
|
90
|
+
inner = '<svg xmlns="http://www.w3.org/2000/svg"><image href="http://e.test/a"/></svg>'
|
|
91
|
+
error = dom(svg(%(<image href="#{b64_svg(inner)}"/>))).errors.first
|
|
92
|
+
expect(error.data[:reason]).to match(/nested href 'http:\/\/e.test\/a'/)
|
|
93
|
+
expect { JSON.generate(error.data) }.not_to raise_error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "limits nesting depth" do
|
|
97
|
+
nest = plain
|
|
98
|
+
4.times do
|
|
99
|
+
nest = %(<svg xmlns="http://www.w3.org/2000/svg"><image href="#{b64_svg(nest)}"/></svg>)
|
|
100
|
+
end
|
|
101
|
+
error = sax(svg(%(<image href="#{b64_svg(nest)}"/>))).errors.first
|
|
102
|
+
expect(error.data[:reason]).to include("nesting exceeds 3")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "ignores empty references like the classifier does" do
|
|
107
|
+
expect(sax(svg('<image href=""/>')).errors).to be_empty
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -74,4 +74,20 @@ RSpec.describe SvgConform::Requirements::LinkValidationRequirement do
|
|
|
74
74
|
expect(requirement).to be_a(described_class)
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
describe "classifier sharing with ExternalReferencesRequirement" do
|
|
79
|
+
it "still reports a missing fragment target while external-reference allows the fragment" do
|
|
80
|
+
svg = '<svg xmlns="http://www.w3.org/2000/svg"><use href="#missing"/></svg>'
|
|
81
|
+
profile = SvgConform::Profile.new(
|
|
82
|
+
name: "p",
|
|
83
|
+
requirements: [
|
|
84
|
+
described_class.new(id: "link"),
|
|
85
|
+
SvgConform::Requirements::ExternalReferencesRequirement.new(id: "ext"),
|
|
86
|
+
],
|
|
87
|
+
)
|
|
88
|
+
ids = SvgConform::SaxDocument.from_content(svg)
|
|
89
|
+
.validate_with_profile(profile).errors.map(&:requirement_id)
|
|
90
|
+
expect(ids).not_to include("ext")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
77
93
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
RSpec.describe SvgConform::Requirements::ProcessingFeaturesRequirement do
|
|
7
|
+
let(:config_path) { "config/requirements/processing_features_static.yml" }
|
|
8
|
+
let(:requirement) { described_class.from_yaml(File.read(config_path)) }
|
|
9
|
+
let(:profile) do
|
|
10
|
+
SvgConform::Profile.new(name: "pf", requirements: [requirement])
|
|
11
|
+
end
|
|
12
|
+
let(:svg_ns) { 'xmlns="http://www.w3.org/2000/svg"' }
|
|
13
|
+
|
|
14
|
+
def svg(body)
|
|
15
|
+
%(<svg #{svg_ns} version="1.1" viewBox="0 0 10 10">#{body}</svg>)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def dom_ids(xml)
|
|
19
|
+
profile.validate(SvgConform::Document.from_content(xml))
|
|
20
|
+
.errors.map(&:requirement_id).sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def sax_ids(xml)
|
|
24
|
+
SvgConform::SaxDocument.from_content(xml).validate_with_profile(profile)
|
|
25
|
+
.errors.map(&:requirement_id).sort
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "loads the three categories from YAML with separate ids and citations" do
|
|
29
|
+
expect(requirement.categories.map(&:name))
|
|
30
|
+
.to eq(%w[script animation interaction])
|
|
31
|
+
expect(requirement.categories.map(&:id)).to eq(
|
|
32
|
+
%w[svg11.processing.script svg11.processing.animation
|
|
33
|
+
svg11.processing.interaction],
|
|
34
|
+
)
|
|
35
|
+
expect(requirement.categories.map(&:citation).uniq.size).to eq(1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "resolves through a profile YAML via the requirement class map" do
|
|
39
|
+
entry = File.read(config_path).lines.each_with_index.map do |l, i|
|
|
40
|
+
i.zero? ? " - #{l}" : " #{l}"
|
|
41
|
+
end.join
|
|
42
|
+
yaml = "name: x\nrequirements:\n#{entry}"
|
|
43
|
+
loaded = SvgConform::Profile.from_yaml(yaml)
|
|
44
|
+
expect(loaded.requirements.first).to be_a(described_class)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "is registered in Requirements.all" do
|
|
48
|
+
expect(SvgConform::Requirements.all).to include(described_class)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
"script element" => ["<script>x()</script>", %w[svg11.processing.script]],
|
|
53
|
+
"load handler" => ['<rect onload="x()" width="1" height="1"/>',
|
|
54
|
+
%w[svg11.processing.script]],
|
|
55
|
+
"animate element" => ['<rect width="1" height="1"><animate attributeName="x" dur="1s"/></rect>',
|
|
56
|
+
%w[svg11.processing.animation svg11.processing.animation]],
|
|
57
|
+
"click handler" => ['<rect onclick="x()" width="1" height="1"/>',
|
|
58
|
+
%w[svg11.processing.interaction]],
|
|
59
|
+
"cursor attribute" => ['<rect cursor="pointer" width="1" height="1"/>',
|
|
60
|
+
%w[svg11.processing.interaction]],
|
|
61
|
+
"clean static" => ['<rect width="1" height="1"/>', []],
|
|
62
|
+
}.each do |label, (body, expected)|
|
|
63
|
+
it "reports #{label} identically through DOM and SAX" do
|
|
64
|
+
xml = svg(body)
|
|
65
|
+
expect(dom_ids(xml)).to eq(expected)
|
|
66
|
+
expect(sax_ids(xml)).to eq(expected)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "attaches the category citation to the error" do
|
|
71
|
+
error = SvgConform::SaxDocument.from_content(svg("<script/>"))
|
|
72
|
+
.validate_with_profile(profile).errors.first
|
|
73
|
+
expect(error.data[:citation]).to eq(requirement.categories.first.citation)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Path is fixed by the card; the cop expects svg11_ instead of svg_1_1_.
|
|
6
|
+
RSpec.describe SvgConform::Requirements::Svg11DocumentRequirement do # rubocop:disable RSpec/SpecFilePathFormat
|
|
7
|
+
let(:svg_ns) { 'xmlns="http://www.w3.org/2000/svg"' }
|
|
8
|
+
let(:profile_path) { "config/profiles/svg_1_1_full_static.yml" }
|
|
9
|
+
|
|
10
|
+
def requirement_for(rule)
|
|
11
|
+
SvgConform::Profile.load_from_file(profile_path).requirements.find do |req|
|
|
12
|
+
req.respond_to?(:rule) && req.rule == rule
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(rule, xml, mode: :sax, serialized: false)
|
|
17
|
+
requirement = requirement_for(rule)
|
|
18
|
+
profile = SvgConform::Profile.new(name: "t", requirements: [requirement])
|
|
19
|
+
if mode == :dom
|
|
20
|
+
profile.validate(SvgConform::Document.from_content(xml)).errors
|
|
21
|
+
else
|
|
22
|
+
SvgConform::SaxDocument.from_content(xml, source_serialized: serialized)
|
|
23
|
+
.validate_with_profile(profile).errors
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def doc(attrs: 'version="1.1"', body: '<rect width="1" height="1"/>', prolog: "")
|
|
28
|
+
%(<?xml version="1.0"?>\n#{prolog}<svg #{svg_ns} #{attrs}>#{body}</svg>)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "defines one entry per matrix row with its own id and citation" do
|
|
32
|
+
entries = SvgConform::Profile.load_from_file(profile_path).requirements
|
|
33
|
+
.grep(described_class)
|
|
34
|
+
expect(entries.map(&:rule)).to eq(described_class::RULES)
|
|
35
|
+
expect(entries.map(&:id)).to eq(
|
|
36
|
+
%w[svg11.document.xml svg11.document.root svg11.document.version
|
|
37
|
+
svg11.document.grammar svg11.document.css2
|
|
38
|
+
svg11.document.xml_stylesheet],
|
|
39
|
+
)
|
|
40
|
+
expect(entries.map(&:citation_uri)).to all(start_with("https://www.w3.org/TR/"))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "is registered in Requirements.all" do
|
|
44
|
+
expect(SvgConform::Requirements.all).to include(described_class)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "rejects an unknown rule" do
|
|
48
|
+
requirement = described_class.new(id: "x", rule: "nope")
|
|
49
|
+
context = SvgConform::ValidationContext.new(nil, nil, source: "<svg/>")
|
|
50
|
+
expect { requirement.validate_source("<svg/>", context) }
|
|
51
|
+
.to raise_error(ArgumentError, /unknown rule/)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
%i[sax dom].each do |mode|
|
|
55
|
+
context "when validating in #{mode} mode" do
|
|
56
|
+
it "accepts a valid document in every rule" do
|
|
57
|
+
described_class::RULES.each do |rule|
|
|
58
|
+
expect(run(rule, doc, mode: mode)).to be_empty, "rule #{rule}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "flags a wrong root namespace" do
|
|
63
|
+
errors = run("root", doc.sub(svg_ns, 'xmlns="http://example.com/x"'), mode: mode)
|
|
64
|
+
expect(errors.map(&:requirement_id)).to eq(["svg11.document.root"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "flags a wrong or missing version" do
|
|
68
|
+
expect(run("version", doc(attrs: 'version="1.0"'), mode: mode).size).to eq(1)
|
|
69
|
+
expect(run("version", doc(attrs: ""), mode: mode).size).to eq(1)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "flags DTD content-model and attribute violations" do
|
|
73
|
+
model = run("grammar", doc(body: "<rect><g/></rect>"), mode: mode)
|
|
74
|
+
attribute = run("grammar", doc(body: '<rect width="1" height="1" bogus="1"/>'), mode: mode)
|
|
75
|
+
expect(model.map(&:requirement_id)).to all(eq("svg11.document.grammar"))
|
|
76
|
+
expect(model).not_to be_empty
|
|
77
|
+
expect(attribute).not_to be_empty
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "does not follow an external entity" do
|
|
81
|
+
xml = "<?xml version=\"1.0\"?>\n<!DOCTYPE svg [<!ENTITY x SYSTEM \"file:///etc/passwd\">]>\n<svg #{svg_ns} version=\"1.1\"><title>&x;</title></svg>"
|
|
82
|
+
errors = run("grammar", xml, mode: mode)
|
|
83
|
+
expect(errors.map(&:message).join).to include("external")
|
|
84
|
+
expect(errors.map(&:message).join).not_to include("root:")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "flags CSS grammar errors in style attributes and style elements" do
|
|
88
|
+
attr = run("css2", doc(body: '<rect width="1" height="1" style="fill: {red"/>'), mode: mode)
|
|
89
|
+
sheet = run("css2", doc(body: "<style>rect { fill: red </style>"), mode: mode)
|
|
90
|
+
expect(attr.size).to be >= 1
|
|
91
|
+
expect(sheet.size).to be >= 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "accepts valid CSS and skips non-CSS style types" do
|
|
95
|
+
css = "<style>rect { fill: red }</style><style type=\"text/x-other\">{{</style>"
|
|
96
|
+
expect(run("css2", doc(body: css), mode: mode)).to be_empty
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "treats text/css media types case-insensitively" do
|
|
100
|
+
upper = run("css2", doc(body: '<style type="TEXT/CSS">rect { fill: red </style>'), mode: mode)
|
|
101
|
+
mixed = run("css2", doc(body: '<style type="Text/Css">rect { fill: red </style>'), mode: mode)
|
|
102
|
+
pi = %(<?xml-stylesheet href="a.css" type="TEXT/CSS"?>\n)
|
|
103
|
+
expect(upper.size).to be >= 1
|
|
104
|
+
expect(mixed.size).to be >= 1
|
|
105
|
+
expect(run("xml_stylesheet", doc(prolog: pi), mode: mode)).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "validates xml-stylesheet pseudo-attributes" do
|
|
109
|
+
good = doc(prolog: %(<?xml-stylesheet href="a.css" type="text/css"?>\n))
|
|
110
|
+
no_href = doc(prolog: %(<?xml-stylesheet type="text/css"?>\n))
|
|
111
|
+
bad_type = doc(prolog: %(<?xml-stylesheet href="a.xsl" type="text/xsl"?>\n))
|
|
112
|
+
garbled = doc(prolog: %(<?xml-stylesheet href=a.css?>\n))
|
|
113
|
+
expect(run("xml_stylesheet", good, mode: mode)).to be_empty
|
|
114
|
+
[no_href, bad_type, garbled].each do |xml|
|
|
115
|
+
expect(run("xml_stylesheet", xml, mode: mode).map(&:requirement_id))
|
|
116
|
+
.to eq(["svg11.document.xml_stylesheet"])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "reports malformed XML through SAX" do
|
|
123
|
+
errors = run("xml", "<svg #{svg_ns}><rect></svg>")
|
|
124
|
+
expect(errors.map(&:requirement_id)).to include("svg11.document.xml")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "cannot prove declaration claims for a re-serialized source" do
|
|
128
|
+
errors = run("xml", doc, serialized: true)
|
|
129
|
+
expect(errors.map(&:requirement_id)).to eq(["svg11.document.xml"])
|
|
130
|
+
expect(errors.first.message).to include("cannot be proved")
|
|
131
|
+
expect(run("xml", doc, serialized: false)).to be_empty
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "reports the same through a DOM node and a serialized SAX source" do
|
|
135
|
+
node = SvgConform::Document.from_content(doc).moxml_document
|
|
136
|
+
dom = SvgConform::Document.from_node(node)
|
|
137
|
+
profile = SvgConform::Profile.new(name: "t", requirements: [requirement_for("xml")])
|
|
138
|
+
expect(profile.validate(dom).errors.map(&:requirement_id))
|
|
139
|
+
.to eq(["svg11.document.xml"])
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -56,12 +56,16 @@ RSpec.describe SvgConform::Requirements::TextAsPathRequirement do
|
|
|
56
56
|
SVG
|
|
57
57
|
|
|
58
58
|
result = validator.validate(svg, profile: :metanorma)
|
|
59
|
-
text_as_path_errors = result.errors.select do |e|
|
|
60
|
-
e.message.include?("rendered as paths")
|
|
61
|
-
end
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
# Text-as-path is a quality advisory declared with severity: warning:
|
|
61
|
+
# it must surface as a warning, never as an error, so that schema-valid
|
|
62
|
+
# documents are not reported as invalid (metanorma-pdfa#106).
|
|
63
|
+
expect(result.errors).to be_empty
|
|
64
|
+
expect(result.warnings.map(&:message)).to include(
|
|
65
|
+
a_string_including("rendered as paths"),
|
|
66
|
+
)
|
|
67
|
+
expect(result.warnings.first.severity).to eq(:warning)
|
|
68
|
+
expect(result).to be_valid
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
it "does not flag SVG with native text elements" do
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "svg_conform/validation/css2_core_grammar"
|
|
5
|
+
|
|
6
|
+
RSpec.describe SvgConform::Validation::Css2CoreGrammar do
|
|
7
|
+
describe ".tokenize" do
|
|
8
|
+
def types(css)
|
|
9
|
+
described_class.tokenize(css).map(&:type)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "classifies the CSS 2 token kinds" do
|
|
13
|
+
css = 'a:hover #id 10 50% 2em url(x.png) rgb( "s" U+0-7F ~= |= @page <!-- -->'
|
|
14
|
+
expect(types(css).reject { |t| t == :s }).to eq(
|
|
15
|
+
[:ident, ":", :ident, :hash, :number, :percentage, :dimension, :uri, :function, :string,
|
|
16
|
+
:unicode_range, :includes, :dashmatch, :atkeyword, :cdo, :cdc],
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "drops comments and tracks line and column" do
|
|
21
|
+
tokens = described_class.tokenize("a /* c\n */ b")
|
|
22
|
+
expect(tokens.map(&:value)).to eq(["a", " ", " ", "b"])
|
|
23
|
+
expect(tokens.last.line).to eq(2)
|
|
24
|
+
expect(tokens.last.column).to eq(5)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "flags unterminated strings and comments" do
|
|
28
|
+
expect(types('"abc')).to eq([:bad_string])
|
|
29
|
+
expect(types("/* abc")).to eq([:bad_comment])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "accepts escapes in identifiers" do
|
|
33
|
+
expect(types('\66 ill')).to eq([:ident])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe ".parse_stylesheet" do
|
|
38
|
+
valid = {
|
|
39
|
+
"empty sheet" => "",
|
|
40
|
+
"comments and CDO/CDC" => "<!-- /* x */ a { fill: red } -->",
|
|
41
|
+
"multiple selectors" => "rect, circle.a > path[x=\"1\"]:first-child { fill: #fff }",
|
|
42
|
+
"empty declaration slots" => "a { ; fill: red;; stroke: blue; }",
|
|
43
|
+
"functions and !important" => "a { fill: rgb(1, 2, 3) !important; stroke: url(#g) }",
|
|
44
|
+
"empty ruleset" => "a { }",
|
|
45
|
+
"@import" => '@import url("x.css") screen;',
|
|
46
|
+
"@media with nested rules" => "@media print { a { x: y } b { x: y } }",
|
|
47
|
+
"@font-face" => '@font-face { font-family: "F"; src: url(f.woff) }',
|
|
48
|
+
"unknown at-rule block" => "@foo bar { baz { qux: 1 } }",
|
|
49
|
+
"selector-less ruleset" => "{ fill: red }",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
valid.each do |name, css|
|
|
53
|
+
it "accepts #{name}" do
|
|
54
|
+
expect(described_class.parse_stylesheet(css)).to be_valid
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
invalid = {
|
|
59
|
+
"missing colon" => ["a { fill red }", /expected ':'/],
|
|
60
|
+
"missing value" => ["a { fill: }", /no value/],
|
|
61
|
+
"missing property" => ["a { : red }", /property name/],
|
|
62
|
+
"unclosed ruleset" => ["a { fill: red", /missing '}'/],
|
|
63
|
+
"unclosed string" => ["a { fill: \"red }", /unterminated string/],
|
|
64
|
+
"unclosed comment" => ["a { fill: red } /* x", /unterminated comment/],
|
|
65
|
+
"unclosed function" => ["a { fill: rgb(1,2 }", /missing '\)'/],
|
|
66
|
+
"selector without block" => ["a b", /missing a '\{' block/],
|
|
67
|
+
"unterminated at-rule" => ["@import url(x.css)", /at-rule/],
|
|
68
|
+
"stray closing brace" => ["a { fill: red } }", /unexpected '\}'/],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
invalid.each do |name, (css, message)|
|
|
72
|
+
it "rejects #{name}" do
|
|
73
|
+
result = described_class.parse_stylesheet(css)
|
|
74
|
+
expect(result).not_to be_valid
|
|
75
|
+
expect(result.errors.map(&:message).join("\n")).to match(message)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "reports the position of the first error" do
|
|
80
|
+
error = described_class.parse_stylesheet("a { fill: red }\nb { fill blue }")
|
|
81
|
+
.errors.first
|
|
82
|
+
expect(error.line).to eq(2)
|
|
83
|
+
expect(error.to_s).to include("line 2")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe ".parse_declarations" do
|
|
88
|
+
it "accepts a style attribute value" do
|
|
89
|
+
css = "fill:red; stroke : url(#a) ; stroke-width:2px;"
|
|
90
|
+
expect(described_class.parse_declarations(css)).to be_valid
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "accepts an empty value and stray semicolons" do
|
|
94
|
+
expect(described_class.parse_declarations("")).to be_valid
|
|
95
|
+
expect(described_class.parse_declarations(" ; ;")).to be_valid
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "rejects a declaration without a colon" do
|
|
99
|
+
result = described_class.parse_declarations("fill red")
|
|
100
|
+
expect(result).not_to be_valid
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "rejects a stray closing brace in a declaration list" do
|
|
104
|
+
result = described_class.parse_declarations("fill:red } stroke:blue")
|
|
105
|
+
expect(result).not_to be_valid
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "rejects a ruleset where a declaration list is expected" do
|
|
109
|
+
expect(described_class.parse_declarations("a { fill: red }")).not_to be_valid
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "does not judge property names or values" do
|
|
113
|
+
expect(described_class.parse_declarations("bogus-prop: nonsense 12qq")).to be_valid
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "svg_conform/validation/svg_1_1_document_conformance"
|
|
8
|
+
|
|
9
|
+
# Path is fixed by the card; the cop expects svg11_ instead of svg_1_1_.
|
|
10
|
+
RSpec.describe SvgConform::Validation::Svg11DocumentConformance do # rubocop:disable RSpec/SpecFilePathFormat
|
|
11
|
+
subject(:conformance) { described_class.new }
|
|
12
|
+
|
|
13
|
+
let(:ns) { %(xmlns="http://www.w3.org/2000/svg" version="1.1") }
|
|
14
|
+
let(:dtd_path) { described_class::DTD_PATH }
|
|
15
|
+
|
|
16
|
+
def svg(body, attrs = ns)
|
|
17
|
+
%(<?xml version="1.0"?>\n<svg #{attrs}>#{body}</svg>)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Library selection record (card 03, "Open implementation question").
|
|
21
|
+
# Probes: valid document, invalid content model, external-entity attempt,
|
|
22
|
+
# Appendix G.2 foreign content, run offline against the vendored flat DTD.
|
|
23
|
+
#
|
|
24
|
+
# Moxml/Leptris 1.9.381 (Leptris::XML::DTD): parses the DTD but ignores
|
|
25
|
+
# conditional sections whose keyword is a parameter entity
|
|
26
|
+
# (<![%svg-structure.module;[ ... ]]>), so no SVG element is declared and
|
|
27
|
+
# every probe, including the invalid ones, reports valid. Rejected.
|
|
28
|
+
# Nokogiri 1.19 (libxml2 2.13, Ruby >= 3.1 with 1.16/1.18): all four
|
|
29
|
+
# probes behave correctly offline, in-process, with NONET and no entity
|
|
30
|
+
# loading. Selected.
|
|
31
|
+
describe "library probes" do
|
|
32
|
+
let(:flat_dtd) { File.read(dtd_path) }
|
|
33
|
+
|
|
34
|
+
it "records that Leptris cannot enforce the flat DTD" do
|
|
35
|
+
require "leptris"
|
|
36
|
+
handle = Leptris::XML::DTD.parse(flat_dtd)
|
|
37
|
+
invalid = Leptris::XML.parse(%(<svg #{ns}><rect width="1" height="1"><g/></rect></svg>))
|
|
38
|
+
|
|
39
|
+
expect(handle.validate(invalid)).to be_empty
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "probe 1: Nokogiri accepts a valid document" do
|
|
43
|
+
expect(conformance.validate(svg(%(<rect width="1" height="1"/>))).issues).to be_empty
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "probe 2: Nokogiri rejects an invalid content model" do
|
|
47
|
+
result = conformance.validate(svg(%(<rect width="1" height="1"><g/></rect>)))
|
|
48
|
+
|
|
49
|
+
expect(result.issues.map(&:kind)).to eq([:dtd])
|
|
50
|
+
expect(result.issues.first.message).to include("rect content does not follow the DTD")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "probe 3: an external entity attempt is rejected without reading the file" do
|
|
54
|
+
Dir.mktmpdir do |dir|
|
|
55
|
+
secret = File.join(dir, "secret.txt")
|
|
56
|
+
File.write(secret, "SECRET-MARKER")
|
|
57
|
+
source = %(<?xml version="1.0"?><!DOCTYPE svg [<!ENTITY x SYSTEM "file://#{secret}">]>) +
|
|
58
|
+
%(<svg #{ns}><title>&x;</title></svg>)
|
|
59
|
+
|
|
60
|
+
result = conformance.validate(source)
|
|
61
|
+
|
|
62
|
+
expect(result.issues.map(&:kind)).to eq([:external_entity])
|
|
63
|
+
expect(result.transformed).to be_nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "probe 4: foreign content validates after the Appendix G.2 transform" do
|
|
68
|
+
source = svg(%(<f:thing f:a="1"><g/></f:thing><metadata><f:m/></metadata>),
|
|
69
|
+
%(#{ns} xmlns:f="urn:foreign"))
|
|
70
|
+
|
|
71
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "#validate" do
|
|
76
|
+
it "reports a missing required attribute" do
|
|
77
|
+
result = conformance.validate(svg(%(<rect height="1"/>)))
|
|
78
|
+
|
|
79
|
+
expect(result.issues.first.message).to include("Element rect does not carry attribute width")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "reports an undeclared attribute" do
|
|
83
|
+
result = conformance.validate(svg(%(<rect width="1" height="1" bogus="x"/>)))
|
|
84
|
+
|
|
85
|
+
expect(result.issues.first.message).to include("No declaration for attribute bogus")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "reports malformed XML" do
|
|
89
|
+
result = conformance.validate(svg(%(<rect width="1" height="1">)))
|
|
90
|
+
|
|
91
|
+
expect(result.issues.first.kind).to eq(:malformed_xml)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "reports a document element outside the SVG namespace" do
|
|
95
|
+
result = conformance.validate(%(<svg xmlns="urn:other" version="1.1"/>))
|
|
96
|
+
|
|
97
|
+
expect(result.issues.map(&:kind)).to eq([:no_svg_root])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "validates xlink attributes under any prefix" do
|
|
101
|
+
source = svg(%(<defs><g id="a"/></defs><use xlink:href="#a"/>),
|
|
102
|
+
%(#{ns} xmlns:xl="http://www.w3.org/1999/xlink"))
|
|
103
|
+
source = source.sub("xlink:href", "xl:href")
|
|
104
|
+
|
|
105
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "validates prefixed SVG elements" do
|
|
109
|
+
source = %(<s:svg xmlns:s="http://www.w3.org/2000/svg" version="1.1"><s:rect width="1" height="1"/></s:svg>)
|
|
110
|
+
|
|
111
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "expands internal entities before validating" do
|
|
115
|
+
source = %(<?xml version="1.0"?><!DOCTYPE svg [<!ENTITY w "5">]><svg #{ns}><rect width="&w;" height="1"/></svg>)
|
|
116
|
+
|
|
117
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "never modifies the caller's source" do
|
|
121
|
+
source = svg(%(<f:x xmlns:f="urn:f"/><rect width="1" height="1"/>)).freeze
|
|
122
|
+
|
|
123
|
+
expect { conformance.validate(source) }.not_to raise_error
|
|
124
|
+
expect(source).to include("<f:x")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "keeps the Appendix G.2 copy free of foreign content" do
|
|
128
|
+
copy = conformance.validate(svg(%(<f:x xmlns:f="urn:f"/>), %(#{ns} f2:a="1" xmlns:f2="urn:f"))).transformed
|
|
129
|
+
|
|
130
|
+
expect(copy).not_to include("f:x")
|
|
131
|
+
expect(copy).not_to include("f2:a")
|
|
132
|
+
expect(copy).to include(%(xmlns:xlink="http://www.w3.org/1999/xlink"))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe "vendored DTD" do
|
|
137
|
+
it "matches the manifest checksum" do
|
|
138
|
+
entry = YAML.safe_load_file(described_class::MANIFEST_PATH, permitted_classes: [Date])["files"].first
|
|
139
|
+
|
|
140
|
+
expect(Digest::SHA256.file(dtd_path).hexdigest).to eq(entry["sha256"])
|
|
141
|
+
expect(entry["source_url"]).to start_with("https://www.w3.org/")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|