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,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
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "SVG 2 secure-static profile" do # rubocop:disable RSpec/DescribeClass
|
|
6
|
+
let(:profile) { SvgConform::Profiles.get("svg_2_secure_static") }
|
|
7
|
+
let(:citation) do
|
|
8
|
+
"https://www.w3.org/TR/2026/CR-SVG2-20261006/conform.html#secure-static-mode"
|
|
9
|
+
end
|
|
10
|
+
let(:root) { "spec/fixtures/profiles/svg_2_secure_static" }
|
|
11
|
+
|
|
12
|
+
def dom_report(path)
|
|
13
|
+
profile.validate(SvgConform::Document.from_file(path))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def sax_report(path)
|
|
17
|
+
SvgConform::SaxDocument.from_file(path).validate_with_profile(profile)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def summary(result)
|
|
21
|
+
result.errors.map do |e|
|
|
22
|
+
[e.requirement_id, e.data[:citation]]
|
|
23
|
+
end.sort
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "is discoverable, opt-in and dated" do
|
|
27
|
+
expect(SvgConform::Profiles.list).to include("svg_2_secure_static")
|
|
28
|
+
expect(profile.specification_uri).to include("CR-SVG2-20261006")
|
|
29
|
+
default = SvgConform::Cli.commands["check"].options[:profile].default
|
|
30
|
+
expect(default).to eq("svg_1_2_rfc")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "defines the four stable requirement ids with the dated citation" do
|
|
34
|
+
categories = profile.requirements.first.categories
|
|
35
|
+
ids = categories.map(&:id) + [profile.requirements.last.id]
|
|
36
|
+
expect(ids).to eq(%w[svg2.secure_static.script svg2.secure_static.animation
|
|
37
|
+
svg2.secure_static.interaction
|
|
38
|
+
svg2.secure_static.external_reference])
|
|
39
|
+
expect(categories.map(&:citation).uniq).to eq([citation])
|
|
40
|
+
expect(profile.requirements.last.citation_uri).to eq(citation)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "uses the unchanged Card 03 requirement class and shape" do
|
|
44
|
+
expect(profile.requirements.first)
|
|
45
|
+
.to be_a(SvgConform::Requirements::ProcessingFeaturesRequirement)
|
|
46
|
+
expect(profile.requirements.first.categories.first.class.attributes.keys)
|
|
47
|
+
.to eq(%i[name id citation description elements attributes
|
|
48
|
+
attribute_patterns])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Dir.glob("spec/fixtures/profiles/svg_2_secure_static/valid/*.svg").each do |path|
|
|
52
|
+
it "accepts #{File.basename(path)} through DOM and SAX" do
|
|
53
|
+
expect(dom_report(path).errors).to be_empty
|
|
54
|
+
expect(sax_report(path).errors).to be_empty
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
"script" => "svg2.secure_static.script",
|
|
60
|
+
"animation" => "svg2.secure_static.animation",
|
|
61
|
+
"interaction" => "svg2.secure_static.interaction",
|
|
62
|
+
}.each do |name, id|
|
|
63
|
+
it "rejects #{name}.svg only with #{id}, matching DOM and SAX" do
|
|
64
|
+
path = "#{root}/invalid/#{name}.svg"
|
|
65
|
+
expect(dom_report(path).errors.map(&:requirement_id).uniq).to eq([id])
|
|
66
|
+
expect(summary(sax_report(path))).to eq(summary(dom_report(path)))
|
|
67
|
+
expect(summary(sax_report(path)).first.last).to eq(citation)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.external_fixtures
|
|
72
|
+
Dir.glob("spec/fixtures/profiles/svg_2_secure_static/invalid/*.svg")
|
|
73
|
+
.map { |f| File.basename(f, ".svg") } - %w[script animation interaction]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "enumerates the external-reference corpus" do
|
|
77
|
+
expect(self.class.external_fixtures.size).to eq(18)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
external_fixtures.each do |name|
|
|
81
|
+
it "rejects #{name}.svg with external_reference, matching DOM and SAX" do
|
|
82
|
+
path = "#{root}/invalid/#{name}.svg"
|
|
83
|
+
expect(dom_report(path).errors.map(&:requirement_id).uniq)
|
|
84
|
+
.to eq(["svg2.secure_static.external_reference"])
|
|
85
|
+
expect(summary(sax_report(path))).to eq(summary(dom_report(path)))
|
|
86
|
+
expect(dom_report(path).errors.first.data[:citation]).to eq(citation)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "emits a LensCore report with the profile citation" do
|
|
91
|
+
report = SvgConform::Diagnostics::LensCoreAdapter.call(
|
|
92
|
+
sax_report("#{root}/invalid/http.svg"),
|
|
93
|
+
)
|
|
94
|
+
expect(report.valid).to be false
|
|
95
|
+
expect(report.profile.id).to eq("svg_2_secure_static")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def inline_ids(body)
|
|
99
|
+
xml = %(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1">#{body}</svg>)
|
|
100
|
+
dom = profile.validate(SvgConform::Document.from_content(xml)).errors
|
|
101
|
+
sax = SvgConform::SaxDocument.from_content(xml)
|
|
102
|
+
.validate_with_profile(profile).errors
|
|
103
|
+
expect(sax.map(&:requirement_id)).to eq(dom.map(&:requirement_id))
|
|
104
|
+
dom.map(&:requirement_id)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
"unlisted pointer handler" => ['<rect onpointerdown="x()"/>',
|
|
109
|
+
%w[svg2.secure_static.interaction]],
|
|
110
|
+
"unlisted drag handler" => ['<rect ondragstart="x()"/>',
|
|
111
|
+
%w[svg2.secure_static.interaction]],
|
|
112
|
+
"listed load handler keeps script id" => ['<rect onload="x()"/>',
|
|
113
|
+
%w[svg2.secure_static.script]],
|
|
114
|
+
"listed click handler is reported once" => ['<rect onclick="x()"/>',
|
|
115
|
+
%w[svg2.secure_static.interaction]],
|
|
116
|
+
"listed begin handler keeps animation id" => ['<rect onbegin="x()"/>',
|
|
117
|
+
%w[svg2.secure_static.animation]],
|
|
118
|
+
"style attribute animation" => ['<rect style="animation: pulse 1s infinite"/>',
|
|
119
|
+
%w[svg2.secure_static.animation]],
|
|
120
|
+
"style attribute vendor transition" => ['<rect style="fill:red;-webkit-transition: all 1s"/>',
|
|
121
|
+
%w[svg2.secure_static.animation]],
|
|
122
|
+
"style attribute escaped animation" => ['<rect style="\\61nimation-name: pulse"/>',
|
|
123
|
+
%w[svg2.secure_static.animation]],
|
|
124
|
+
"style element animation" => ["<style>rect { animation: pulse 1s }</style>",
|
|
125
|
+
%w[svg2.secure_static.animation]],
|
|
126
|
+
"style element keyframes" => ["<style>@keyframes pulse { from { opacity: 0 } }</style>",
|
|
127
|
+
%w[svg2.secure_static.animation]],
|
|
128
|
+
"style element nested media animation" => ["<style>@media print { a { transition: all 1s } }</style>",
|
|
129
|
+
%w[svg2.secure_static.animation]],
|
|
130
|
+
"static css is accepted" => ['<style>rect { fill: red } a:hover { fill: blue }</style><rect style="fill: red"/>', []],
|
|
131
|
+
"animation word in selector or value is accepted" => ['<style>.animation { fill: red }</style><rect style="font-family: animation"/>', []],
|
|
132
|
+
}.each do |label, (body, expected)|
|
|
133
|
+
it "#{label}: #{expected.inspect} through DOM and SAX" do
|
|
134
|
+
expect(inline_ids(body)).to eq(expected)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "reports external CSS references hidden by escapes through DOM and SAX" do
|
|
139
|
+
bodies = [
|
|
140
|
+
'<rect style="fill:u\\72l(https://example.com/x.png)"/>',
|
|
141
|
+
'<rect style="fill:\\75rl(https://example.com/x.png)"/>',
|
|
142
|
+
'<style>@\\69mport "https://example.com/a.css";</style>',
|
|
143
|
+
'<style>@IMP\\4frt url(https://example.com/a.css);</style>',
|
|
144
|
+
]
|
|
145
|
+
bodies.each do |body|
|
|
146
|
+
expect(inline_ids(body)).to eq(["svg2.secure_static.external_reference"]),
|
|
147
|
+
body
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
RSpec.describe SvgConform::Profiles do
|
|
7
|
+
let(:validator) { SvgConform::Validator.new(mode: :sax) }
|
|
8
|
+
let(:profile) { described_class.get(:metanorma) }
|
|
9
|
+
let(:plantuml_attributes) { %w[contentstyletype zoomandpan title textlength lengthadjust] }
|
|
10
|
+
let(:svg) do
|
|
11
|
+
<<~SVG
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
13
|
+
version="1.2" baseProfile="tiny" viewBox="0 0 120 30"
|
|
14
|
+
contentStyleType="text/css" zoomAndPan="magnify">
|
|
15
|
+
<a xlink:href="https://example.test/UnitType" title="UnitType">
|
|
16
|
+
<text id="label" x="0" y="12" textLength="100" lengthAdjust="spacing">UnitType</text>
|
|
17
|
+
</a>
|
|
18
|
+
</svg>
|
|
19
|
+
SVG
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "accepts the five explicit layout and link attributes" do
|
|
23
|
+
result = validator.validate(svg, profile: profile)
|
|
24
|
+
expect(result.errors.map(&:message)).to be_empty
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
%w[svg text a].each do |element|
|
|
28
|
+
it "still rejects unrelated attributes on #{element}" do
|
|
29
|
+
invalid = svg.sub("<#{element} ", "<#{element} unsupported-plantuml-setting=\"no\" ")
|
|
30
|
+
result = validator.validate(invalid, profile: profile)
|
|
31
|
+
expect(result.errors.map(&:message).join).to include("unsupported-plantuml-setting")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
{ "svg" => "textLength", "text" => "zoomAndPan", "a" => "lengthAdjust" }.each do |element, attribute|
|
|
36
|
+
it "rejects #{attribute} on #{element}" do
|
|
37
|
+
invalid = svg.sub("<#{element} ", "<#{element} #{attribute}=\"1\" ")
|
|
38
|
+
result = validator.validate(invalid, profile: profile)
|
|
39
|
+
expect(result.errors.map(&:message).join.downcase).to include("attribute '#{attribute.downcase}' is not allowed")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
%i[svg_1_2_rfc svg_1_2_rfc_with_rdf].each do |strict_profile|
|
|
44
|
+
it "keeps #{strict_profile} attribute restrictions unchanged" do
|
|
45
|
+
result = validator.validate(svg, profile: strict_profile)
|
|
46
|
+
messages = result.errors.map(&:message).join.downcase
|
|
47
|
+
plantuml_attributes.each do |attribute|
|
|
48
|
+
expect(messages).to include("attribute '#{attribute}' is not allowed")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "preserves native PlantUML text measurements and links during remediation" do
|
|
54
|
+
source = File.read("spec/fixtures/plantuml/unit-type.svg")
|
|
55
|
+
document = SvgConform::Document.from_content(source)
|
|
56
|
+
result = validator.validate(source, profile: profile)
|
|
57
|
+
SvgConform::RemediationEngine.new(profile).apply_remediations(document, result)
|
|
58
|
+
output = document.root.to_xml
|
|
59
|
+
errors = validator.validate(output, profile: profile).errors
|
|
60
|
+
.reject { |error| error.violation_type == :reference_violation }
|
|
61
|
+
|
|
62
|
+
# The fixture's fragment targets belong to the surrounding HTML document.
|
|
63
|
+
expect(errors.map(&:message)).to be_empty
|
|
64
|
+
expect(text_layout(output)).to eq(text_layout(source))
|
|
65
|
+
expect(text_layout(output).size).to eq(28)
|
|
66
|
+
expect(diagram_links(output)).to eq(diagram_links(source))
|
|
67
|
+
expect(diagram_links(output).size).to eq(12)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def text_layout(xml)
|
|
71
|
+
Nokogiri::XML(xml).xpath("//svg:text", "svg" => "http://www.w3.org/2000/svg").map do |node|
|
|
72
|
+
[node.text, node["x"], node["y"], node["textLength"], node["lengthAdjust"]]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def diagram_links(xml)
|
|
77
|
+
Nokogiri::XML(xml).xpath("//svg:a", "svg" => "http://www.w3.org/2000/svg").map do |node|
|
|
78
|
+
[node["title"], node.attribute_with_ns("href", "http://www.w3.org/1999/xlink")&.value]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "svg_conform/quality_metrics/configuration"
|
|
5
|
+
require "psych"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
|
|
8
|
+
RSpec.describe SvgConform::QualityMetrics::Configuration do
|
|
9
|
+
describe ".default" do
|
|
10
|
+
let(:hash) { described_class.default.instance_variable_get(:@hash) }
|
|
11
|
+
|
|
12
|
+
it "loads bundled violation types as Symbols" do
|
|
13
|
+
types = hash.fetch("severity_levels").values.flat_map do |level|
|
|
14
|
+
level.fetch("violation_types")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
expect(types).to include(:reference_violation)
|
|
18
|
+
expect(types).to all(be_a(Symbol))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "preserves the entire Psych-loaded configuration" do
|
|
22
|
+
expected = Psych.safe_load_file(described_class::DEFAULT_CONFIG_PATH,
|
|
23
|
+
permitted_classes: [Symbol],
|
|
24
|
+
permitted_symbols: [], aliases: true)
|
|
25
|
+
|
|
26
|
+
expect(hash).to eq(expected)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ".from_yaml" do
|
|
31
|
+
def load_yaml(source)
|
|
32
|
+
Tempfile.create(["quality_metrics", ".yml"]) do |file|
|
|
33
|
+
file.write(source)
|
|
34
|
+
file.flush
|
|
35
|
+
described_class.from_yaml(file.path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "preserves quoted Symbol values" do
|
|
40
|
+
pending "yeptris differs from Psych: quoted Symbols retain quotes; upstream issue to be filed, see PR description"
|
|
41
|
+
|
|
42
|
+
config = load_yaml('a: :"x y"')
|
|
43
|
+
|
|
44
|
+
expect(config.instance_variable_get(:@hash)).to eq("a" => :"x y")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "rejects disallowed dates with the yeptris error naming Date" do
|
|
48
|
+
expect { load_yaml("f: 2024-01-02\n") }
|
|
49
|
+
.to raise_error(Yeptris::Psych::DisallowedClass, /Date/)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "rejects disallowed times with the yeptris error naming Time" do
|
|
53
|
+
pending "yeptris differs from Psych: disallowed Time is reported as Date; upstream issue to be filed, see PR description"
|
|
54
|
+
|
|
55
|
+
expect { load_yaml("f: 2024-01-02 10:00:00\n") }
|
|
56
|
+
.to raise_error(Yeptris::Psych::DisallowedClass, /Time/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "rejects invalid YAML with the yeptris syntax error" do
|
|
60
|
+
expect { load_yaml("a: [\n") }.to raise_error(Yeptris::Psych::SyntaxError)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "resolves anchors and aliases" do
|
|
64
|
+
path = File.expand_path("../../fixtures/quality_metrics/aliases.yml", __dir__)
|
|
65
|
+
hash = described_class.from_yaml(path).instance_variable_get(:@hash)
|
|
66
|
+
|
|
67
|
+
expect(hash.fetch("remediable_types")).to eq([:reference_violation])
|
|
68
|
+
expect(hash.dig("severity_levels", "critical", "violation_types"))
|
|
69
|
+
.to eq(hash.fetch("remediable_types"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "rejects a non-permitted class with the yeptris error" do
|
|
73
|
+
Tempfile.create(["quality_metrics", ".yml"]) do |file|
|
|
74
|
+
file.write("--- !ruby/object:Object {}\n")
|
|
75
|
+
file.flush
|
|
76
|
+
|
|
77
|
+
expect { described_class.from_yaml(file.path) }
|
|
78
|
+
.to raise_error(Yeptris::Psych::DisallowedClass)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
RSpec.describe SvgConform::References::DataUriReference do
|
|
8
|
+
def ref(value)
|
|
9
|
+
SvgConform::References::ReferenceClassifier.classify(
|
|
10
|
+
value, element_name: "image", attribute_name: "href"
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def chunk(type, data)
|
|
15
|
+
[data.bytesize].pack("N") + type + data + [Zlib.crc32(type + data)].pack("N")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:png) do
|
|
19
|
+
"\x89PNG\r\n\x1A\n".b +
|
|
20
|
+
chunk("IHDR", [1, 1, 8, 2, 0, 0, 0].pack("NNCCCCC")) +
|
|
21
|
+
chunk("IDAT", Zlib::Deflate.deflate("\x00\xFF\x00\x00".b)) +
|
|
22
|
+
chunk("IEND", "")
|
|
23
|
+
end
|
|
24
|
+
let(:jpeg) { "\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xFF\xD9".b }
|
|
25
|
+
|
|
26
|
+
it "decodes base64 and percent-encoded payloads to the same bytes" do
|
|
27
|
+
pct = png.each_byte.map { |b| format("%%%02X", b) }.join
|
|
28
|
+
expect(ref("data:image/png;base64,#{Base64.strict_encode64(png)}").payload)
|
|
29
|
+
.to eq(png)
|
|
30
|
+
expect(ref("data:image/png,#{pct}").payload).to eq(png)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "reports base64? and a normalized media type" do
|
|
34
|
+
r = ref("data:IMAGE/PNG;base64,AAAA")
|
|
35
|
+
expect(r.base64?).to be true
|
|
36
|
+
expect(r.normalized_media_type).to eq("image/png")
|
|
37
|
+
expect(ref("data:,hello").normalized_media_type).to eq("text/plain")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "accepts well-formed PNG and JPEG payloads" do
|
|
41
|
+
expect(ref("data:image/png;base64,#{Base64.strict_encode64(png)}")
|
|
42
|
+
.raster_error).to be_nil
|
|
43
|
+
expect(ref("data:image/jpeg;base64,#{Base64.strict_encode64(jpeg)}")
|
|
44
|
+
.raster_error).to be_nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
"data:image/png;base64," => /empty/,
|
|
49
|
+
"data:image/png;base64,!!!" => /malformed base64/,
|
|
50
|
+
"data:image/png,%G1" => /percent/,
|
|
51
|
+
"data:image/png" => /separator/,
|
|
52
|
+
"data:image/png;base64,bm90IGEgcG5n" => /not a PNG/,
|
|
53
|
+
"data:image/jpeg;base64,bm90IGEganBn" => /not a JPEG/,
|
|
54
|
+
}.each do |value, pattern|
|
|
55
|
+
it "rejects #{value}" do
|
|
56
|
+
expect(ref(value).raster_error).to match(pattern)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
"APP0 declaring more bytes than present" => "\xFF\xD8\xFF\xE0\x00\x10\xFF\xD9",
|
|
62
|
+
"segment length below two" => "\xFF\xD8\xFF\xE0\x00\x01\xFF\xD9",
|
|
63
|
+
"marker without length" => "\xFF\xD8\xFF",
|
|
64
|
+
"stray non-marker byte" => "\xFF\xD8\xFF\xE0\x00\x02\x41\xFF\xD9",
|
|
65
|
+
"end marker inside the data before the end" => "\xFF\xD8\xFF\xD9\x00\x00",
|
|
66
|
+
"no end of image" => "\xFF\xD8\xFF\xE0\x00\x02",
|
|
67
|
+
"SOS scan without end of image" => "\xFF\xD8\xFF\xDA\x00\x02\x01\x02\x03",
|
|
68
|
+
}.each do |label, bytes|
|
|
69
|
+
it "rejects a malformed JPEG (#{label}) in base64 and percent forms" do
|
|
70
|
+
raw = bytes.b
|
|
71
|
+
pct = raw.each_byte.map { |b| format("%%%02X", b) }.join
|
|
72
|
+
expect(ref("data:image/jpeg;base64,#{Base64.strict_encode64(raw)}").raster_error)
|
|
73
|
+
.to include("JPEG")
|
|
74
|
+
expect(ref("data:image/jpeg,#{pct}").raster_error).to include("JPEG")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "accepts a JPEG with a scan, byte stuffing and restart markers" do
|
|
79
|
+
scan = "\xFF\xD8\xFF\xE0\x00\x04\x00\x00\xFF\xDA\x00\x04\x01\x02\x11\xFF\x00\x22\xFF\xD0\x33\xFF\xD9".b
|
|
80
|
+
expect(ref("data:image/jpeg;base64,#{Base64.strict_encode64(scan)}").raster_error)
|
|
81
|
+
.to be_nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "detects PNG checksum corruption and truncation" do
|
|
85
|
+
bad = png.dup
|
|
86
|
+
bad.setbyte(20, bad.getbyte(20) ^ 0xFF)
|
|
87
|
+
expect(ref("data:image/png;base64,#{Base64.strict_encode64(bad)}")
|
|
88
|
+
.raster_error).to match(/checksum|IHDR/)
|
|
89
|
+
cut = png.byteslice(0, png.bytesize - 6)
|
|
90
|
+
expect(ref("data:image/png;base64,#{Base64.strict_encode64(cut)}")
|
|
91
|
+
.raster_error).not_to be_nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "stays internal in validation_scope" do
|
|
95
|
+
expect(ref("data:image/png,x").validation_scope).to eq(:internal)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -203,4 +203,34 @@ RSpec.describe "Reference Manifest Integration" do
|
|
|
203
203
|
expect(manifest_data).to have_key(:statistics)
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
|
+
|
|
207
|
+
describe "secure-static profile shares the classifier" do
|
|
208
|
+
it "registers the same reference types the link requirement sees" do
|
|
209
|
+
svg = <<~SVG
|
|
210
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
|
|
211
|
+
<rect id="a" width="1" height="1"/>
|
|
212
|
+
<use href="#a"/>
|
|
213
|
+
<use href="other.svg#a"/>
|
|
214
|
+
</svg>
|
|
215
|
+
SVG
|
|
216
|
+
profile = SvgConform::Profile.new(
|
|
217
|
+
name: "both",
|
|
218
|
+
requirements: [
|
|
219
|
+
SvgConform::Requirements::LinkValidationRequirement.new(id: "link"),
|
|
220
|
+
SvgConform::Requirements::ExternalReferencesRequirement.new(
|
|
221
|
+
id: "svg2.secure_static.external_reference",
|
|
222
|
+
),
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
result = SvgConform::SaxDocument.from_content(svg)
|
|
226
|
+
.validate_with_profile(profile)
|
|
227
|
+
manifest = result.reference_manifest.to_h
|
|
228
|
+
types = (manifest[:internal_references] + manifest[:external_references])
|
|
229
|
+
.map { |r| r[:type] }
|
|
230
|
+
expect(types).to contain_exactly("InternalFragmentReference",
|
|
231
|
+
"RelativePathReference")
|
|
232
|
+
expect(result.errors.map(&:requirement_id))
|
|
233
|
+
.to include("svg2.secure_static.external_reference")
|
|
234
|
+
end
|
|
235
|
+
end
|
|
206
236
|
end
|
|
@@ -138,5 +138,22 @@ RSpec.describe SvgConform::References::ReferenceClassifier do
|
|
|
138
138
|
expect(ref.line_number).to eq(42)
|
|
139
139
|
expect(ref.column_number).to eq(15)
|
|
140
140
|
end
|
|
141
|
+
|
|
142
|
+
it "keeps typed returns and the classify signature for secure-static inputs" do
|
|
143
|
+
expect(described_class.method(:classify).parameters).to eq(
|
|
144
|
+
[%i[req href_value], %i[keyreq element_name], %i[keyreq attribute_name],
|
|
145
|
+
%i[key line_number], %i[key column_number]],
|
|
146
|
+
)
|
|
147
|
+
{
|
|
148
|
+
"//example.com/a.png" => SvgConform::References::RelativePathReference,
|
|
149
|
+
"other.svg#id" => SvgConform::References::RelativePathReference,
|
|
150
|
+
"file:///etc/passwd#id" => SvgConform::References::RelativePathReference,
|
|
151
|
+
"DATA:image/png,x" => SvgConform::References::DataUriReference,
|
|
152
|
+
}.each do |value, klass|
|
|
153
|
+
ref = described_class.classify(value, element_name: "use",
|
|
154
|
+
attribute_name: "href")
|
|
155
|
+
expect(ref).to be_a(klass), value
|
|
156
|
+
end
|
|
157
|
+
end
|
|
141
158
|
end
|
|
142
159
|
end
|