171 lines
6.0 KiB
TypeScript
171 lines
6.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { defaultSvgLimits } from "../../src/app/limits";
|
|
import { parseSvgSource } from "../../src/document/source-parser";
|
|
import {
|
|
applySourcePatches,
|
|
patchAttribute,
|
|
} from "../../src/document/source-patcher";
|
|
|
|
const source = `<?xml version="1.0"?>
|
|
<!--preserve me-->
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80">
|
|
<vendor:thing xmlns:vendor="urn:vendor" unusual="yes"><circle id="dot" cx="10" cy="20" r="4"/></vendor:thing>
|
|
</svg>`;
|
|
|
|
describe("source-faithful SVG parsing", () => {
|
|
it("retains canonical source and maps unknown elements to exact ranges", () => {
|
|
const result = parseSvgSource(source, 7);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.semantic?.source).toBe(source);
|
|
expect(result.semantic?.revision).toBe(7);
|
|
const vendor = [...result.semantic!.nodes.values()].find(
|
|
(node) => node.name === "vendor:thing",
|
|
)!;
|
|
expect(
|
|
source.slice(vendor.sourceRange.full.from, vendor.sourceRange.full.to),
|
|
).toContain("vendor:thing");
|
|
expect(vendor.attributes.unusual).toBe("yes");
|
|
});
|
|
|
|
it("rejects malformed XML without constructing a semantic model", () => {
|
|
const result = parseSvgSource(
|
|
'<svg xmlns="http://www.w3.org/2000/svg"><g>',
|
|
2,
|
|
);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.semantic).toBeNull();
|
|
expect(result.diagnostics.some((item) => item.severity === "error")).toBe(
|
|
true,
|
|
);
|
|
expect(result.diagnostics).toContainEqual(
|
|
expect.objectContaining({ code: "xml-unclosed-element" }),
|
|
);
|
|
});
|
|
|
|
it("preserves but refuses entity declarations before invoking the XML parser", () => {
|
|
const entitySource = `<!DOCTYPE svg [<!ENTITY local "blocked">]><svg xmlns="http://www.w3.org/2000/svg"><text>&local;</text></svg>`;
|
|
const result = parseSvgSource(entitySource, 8);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.semantic).toBeNull();
|
|
expect(result.source).toBe(entitySource);
|
|
expect(result.diagnostics).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "entity-declaration-blocked",
|
|
severity: "error",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("preserves a simple doctype while excluding it from the editing parser", () => {
|
|
const doctypeSource = `<!DOCTYPE svg SYSTEM "https://invalid.example/svg.dtd"><svg xmlns="http://www.w3.org/2000/svg"><rect width="2" height="3"/></svg>`;
|
|
const result = parseSvgSource(doctypeSource, 9);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.semantic?.source).toBe(doctypeSource);
|
|
expect(result.diagnostics).toContainEqual(
|
|
expect.objectContaining({
|
|
code: "doctype-preserved",
|
|
severity: "warning",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("treats duplicate IDs as editable diagnostics, not XML invalidity", () => {
|
|
const result = parseSvgSource(
|
|
'<svg xmlns="http://www.w3.org/2000/svg"><g id="same"/><path id="same" d="M0 0L1 1"/></svg>',
|
|
3,
|
|
);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.semantic).not.toBeNull();
|
|
expect(result.diagnostics).toContainEqual(
|
|
expect.objectContaining({ code: "duplicate-id", severity: "warning" }),
|
|
);
|
|
});
|
|
|
|
it("patches one attribute while preserving comments, quote style and unknown content", () => {
|
|
const result = parseSvgSource(source, 1);
|
|
const dot = [...result.semantic!.nodes.values()].find(
|
|
(node) => node.id === "dot",
|
|
)!;
|
|
const patch = patchAttribute(source, dot, "cx", "42", result.preferences)!;
|
|
const next = applySourcePatches(source, [patch]);
|
|
expect(next).toBe(source.replace('cx="10"', 'cx="42"'));
|
|
expect(next).toContain("<!--preserve me-->");
|
|
expect(next).toContain("vendor:thing");
|
|
});
|
|
|
|
it("rejects overlapping or stale patches", () => {
|
|
expect(() =>
|
|
applySourcePatches("abcdef", [
|
|
{ from: 1, to: 4, insert: "x", label: "first" },
|
|
{ from: 3, to: 5, insert: "y", label: "second" },
|
|
]),
|
|
).toThrow(/overlapping/u);
|
|
});
|
|
|
|
it("enforces each configurable structural resource limit", () => {
|
|
const constrained = {
|
|
...defaultSvgLimits,
|
|
maximumAttributeLength: 12,
|
|
maximumPathCommandsPerPath: 2,
|
|
maximumPathCommandsTotal: 2,
|
|
maximumCssRules: 1,
|
|
maximumReferences: 0,
|
|
maximumAnimations: 0,
|
|
maximumFilterPrimitives: 0,
|
|
maximumTextLength: 4,
|
|
maximumEmbeddedResourceBytes: 8,
|
|
};
|
|
const result = parseSvgSource(
|
|
`<svg xmlns="http://www.w3.org/2000/svg">
|
|
<style>.a{fill:red}.b{stroke:blue}</style>
|
|
<defs><filter id="fx"><feGaussianBlur stdDeviation="1"/></filter></defs>
|
|
<path d="M0 0L1 1L2 2" filter="url(#fx)"/>
|
|
<image href="data:image/png;base64,AAAAAAAAAAAA"/>
|
|
<animate attributeName="opacity" values="0;1"/>
|
|
<text>longer text</text>
|
|
</svg>`,
|
|
9,
|
|
constrained,
|
|
);
|
|
const codes = result.diagnostics.map((item) => item.code);
|
|
expect(result.valid).toBe(false);
|
|
expect(codes).toEqual(
|
|
expect.arrayContaining([
|
|
"attribute-length-limit",
|
|
"embedded-resource-limit",
|
|
"path-command-per-element-limit",
|
|
"path-command-limit",
|
|
"css-rule-limit",
|
|
"reference-limit",
|
|
"animation-limit",
|
|
"filter-primitive-limit",
|
|
"text-length-limit",
|
|
"total-text-limit",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("counts SVG path commands without mistaking exponent notation for commands", () => {
|
|
const result = parseSvgSource(
|
|
'<svg xmlns="http://www.w3.org/2000/svg"><path d="M1e2 0L2e2 1"/></svg>',
|
|
4,
|
|
);
|
|
expect(result.semantic?.metrics.pathCommandCount).toBe(2);
|
|
});
|
|
|
|
it("counts implicit path groups and packed arc flags as resolved commands", () => {
|
|
const result = parseSvgSource(
|
|
'<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 0 1 1 2 2 A5 6 0 0110 20"/></svg>',
|
|
5,
|
|
{ ...defaultSvgLimits, maximumPathCommandsPerPath: 3 },
|
|
);
|
|
|
|
expect(result.semantic?.metrics.pathCommandCount).toBe(4);
|
|
expect(result.diagnostics).toContainEqual(
|
|
expect.objectContaining({ code: "path-command-per-element-limit" }),
|
|
);
|
|
});
|
|
});
|