Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,327 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
SVG_COMPRESS_CODES,
SvgCompressError,
} from "../../error-codes/error-codes.js";
import { parseSvg, serializeSvg } from "./svg.js";
describe("parseSvg", () => {
it("parses a minimal SVG element", { timeout: 5000 }, () => {
const node = parseSvg('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
expect(node.type).toBe("element");
expect(node.name).toBe("svg");
expect(node.attributes).toEqual({
xmlns: "http://www.w3.org/2000/svg",
});
expect(node.children).toEqual([]);
});
it("parses self-closing elements", { timeout: 5000 }, () => {
const node = parseSvg('<svg><circle cx="50" cy="50" r="40"/></svg>');
expect(node.children).toHaveLength(1);
const circle = node.children![0]!;
expect(circle.type).toBe("element");
expect(circle.name).toBe("circle");
expect(circle.attributes).toEqual({ cx: "50", cy: "50", r: "40" });
expect(circle.children).toEqual([]);
});
it("parses nested elements", { timeout: 5000 }, () => {
const svg =
'<svg><g><rect x="0" y="0" width="100" height="100"/></g></svg>';
const node = parseSvg(svg);
const g = node.children![0]!;
expect(g.name).toBe("g");
const rect = g.children![0]!;
expect(rect.name).toBe("rect");
expect(rect.attributes?.x).toBe("0");
});
it("parses text nodes", { timeout: 5000 }, () => {
const node = parseSvg("<svg><text>Hello world</text></svg>");
const text = node.children![0]!;
expect(text.name).toBe("text");
const textChild = text.children![0]!;
expect(textChild.type).toBe("text");
expect(textChild.value).toBe("Hello world");
});
it("parses XML comments", { timeout: 5000 }, () => {
const node = parseSvg("<svg><!-- a comment --></svg>");
const comment = node.children![0]!;
expect(comment.type).toBe("comment");
expect(comment.value).toBe(" a comment ");
});
it("parses CDATA sections", { timeout: 5000 }, () => {
const node = parseSvg("<svg><![CDATA[raw content]]></svg>");
const cdata = node.children![0]!;
expect(cdata.type).toBe("cdata");
expect(cdata.value).toBe("raw content");
});
it("parses doctype declarations", { timeout: 5000 }, () => {
const input =
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg></svg>';
const node = parseSvg(input);
expect(node.type).toBe("element");
expect(node.name).toBe("svg");
});
it("parses XML declaration", { timeout: 5000 }, () => {
const node = parseSvg(
'<?xml version="1.0" encoding="UTF-8"?><svg></svg>'
);
expect(node.name).toBe("svg");
});
it("parses attributes with single quotes", { timeout: 5000 }, () => {
const node = parseSvg("<svg><rect x='10' y='20'/></svg>");
const rect = node.children![0]!;
expect(rect.attributes?.x).toBe("10");
expect(rect.attributes?.y).toBe("20");
});
it("parses attributes with no value (boolean)", { timeout: 5000 }, () => {
const node = parseSvg("<svg><use disabled/></svg>");
const use = node.children![0]!;
expect(use.attributes?.disabled).toBe("");
});
it("parses multiple children", { timeout: 5000 }, () => {
const node = parseSvg(
"<svg><circle r='5'/><rect width='10'/><path d='M0 0'/></svg>"
);
expect(node.children).toHaveLength(3);
});
it("parses whitespace-only text nodes", { timeout: 5000 }, () => {
const node = parseSvg("<svg>\n <circle r='5'/>\n</svg>");
const nonElement = node.children!.filter((c) => c.type !== "element");
expect(nonElement.every((n) => n.type === "text")).toBe(true);
});
it("handles deeply nested structure", { timeout: 5000 }, () => {
const svg = "<svg><g><g><g><rect/></g></g></g></svg>";
const node = parseSvg(svg);
const inner =
node.children![0]!.children![0]!.children![0]!.children![0]!;
expect(inner.name).toBe("rect");
});
it("throws PARSE_FAILED on unclosed tag", { timeout: 5000 }, () => {
expect(() => parseSvg("<svg><circle>")).toThrow(SvgCompressError);
try {
parseSvg("<svg><circle>");
} catch (e) {
expect((e as SvgCompressError).code).toBe(
SVG_COMPRESS_CODES.PARSE_FAILED
);
}
});
it("throws PARSE_FAILED on mismatched closing tag", {
timeout: 5000,
}, () => {
expect(() => parseSvg("<svg><g></h></svg>")).toThrow(SvgCompressError);
});
it("throws PARSE_FAILED on closing tag with empty stack", {
timeout: 5000,
}, () => {
expect(() => parseSvg("</svg>")).toThrow(SvgCompressError);
try {
parseSvg("</svg>");
} catch (e) {
expect((e as SvgCompressError).code).toBe(
SVG_COMPRESS_CODES.PARSE_FAILED
);
expect((e as SvgCompressError).message).toContain("none");
}
});
it("throws PARSE_FAILED on empty input", { timeout: 5000 }, () => {
expect(() => parseSvg("")).toThrow(SvgCompressError);
});
it("throws PARSE_FAILED on non-SVG input (no root element)", {
timeout: 5000,
}, () => {
expect(() => parseSvg("just text")).toThrow(SvgCompressError);
});
it("parses attributes with namespace prefixes", { timeout: 5000 }, () => {
const node = parseSvg(
'<svg xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="#id"/></svg>'
);
const use = node.children![0]!;
expect(use.attributes?.["xlink:href"]).toBe("#id");
});
it("parses attribute values with encoded entities", {
timeout: 5000,
}, () => {
const node = parseSvg('<svg title="a &amp; b"></svg>');
expect(node.attributes?.title).toBe("a & b");
});
it("parses multiple attributes on same element", { timeout: 5000 }, () => {
const node = parseSvg(
'<svg width="100" height="200" viewBox="0 0 100 200" xmlns="http://www.w3.org/2000/svg"></svg>'
);
expect(node.attributes?.width).toBe("100");
expect(node.attributes?.height).toBe("200");
expect(node.attributes?.viewBox).toBe("0 0 100 200");
});
it("throws PARSE_FAILED on unclosed processing instruction", {
timeout: 5000,
}, () => {
expect(() => parseSvg("<?xml version='1.0'<svg/>")).toThrow(
SvgCompressError
);
});
it("throws PARSE_FAILED on unclosed comment", { timeout: 5000 }, () => {
expect(() => parseSvg("<!-- unclosed comment")).toThrow(
SvgCompressError
);
});
it("throws PARSE_FAILED on unclosed CDATA", { timeout: 5000 }, () => {
expect(() => parseSvg("<svg><![CDATA[unclosed")).toThrow(
SvgCompressError
);
});
it("throws PARSE_FAILED on unclosed DOCTYPE", { timeout: 5000 }, () => {
expect(() => parseSvg("<!DOCTYPE svg unclosed")).toThrow(
SvgCompressError
);
});
it("throws PARSE_FAILED on unclosed closing tag", { timeout: 5000 }, () => {
expect(() => parseSvg("<svg></svg")).toThrow(SvgCompressError);
try {
parseSvg("<svg></svg");
} catch (e) {
expect((e as SvgCompressError).code).toBe(
SVG_COMPRESS_CODES.PARSE_FAILED
);
}
});
it("skips non-DOCTYPE declarations (e.g. <!ENTITY>)", {
timeout: 5000,
}, () => {
const node = parseSvg("<!ENTITY foo bar><svg/>");
expect(node.name).toBe("svg");
});
it("decodes hex character entities (&#xHH;)", { timeout: 5000 }, () => {
const node = parseSvg('<svg title="&#x41;&#x42;"/>');
expect(node.attributes?.title).toBe("AB");
});
it("decodes decimal character entities (&#DD;)", { timeout: 5000 }, () => {
const node = parseSvg('<svg title="&#65;&#66;"/>');
expect(node.attributes?.title).toBe("AB");
});
});
describe("serializeSvg", () => {
it("serializes a minimal SVG element", { timeout: 5000 }, () => {
const node = parseSvg('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
const result = serializeSvg(node);
expect(result).toBe('<svg xmlns="http://www.w3.org/2000/svg"/>');
});
it("serializes self-closing elements with children empty", {
timeout: 5000,
}, () => {
const node = parseSvg('<svg><circle r="5"/></svg>');
const result = serializeSvg(node);
expect(result).toContain('<circle r="5"/>');
});
it("serializes text nodes", { timeout: 5000 }, () => {
const node = parseSvg("<svg><text>Hello</text></svg>");
const result = serializeSvg(node);
expect(result).toContain("<text>Hello</text>");
});
it("serializes comment nodes", { timeout: 5000 }, () => {
const node = parseSvg("<svg><!-- note --></svg>");
const result = serializeSvg(node);
expect(result).toContain("<!-- note -->");
});
it("serializes CDATA nodes", { timeout: 5000 }, () => {
const node = parseSvg("<svg><![CDATA[data]]></svg>");
const result = serializeSvg(node);
expect(result).toContain("<![CDATA[data]]>");
});
it("serializes elements with children (non-self-closing)", {
timeout: 5000,
}, () => {
const node = parseSvg("<svg><g><rect/></g></svg>");
const result = serializeSvg(node);
expect(result).toContain("<g><rect/></g>");
});
it("produces compact output (no extra whitespace)", {
timeout: 5000,
}, () => {
const _node = parseSvg("<svg>\n <rect/>\n</svg>");
const result = serializeSvg({
type: "element",
name: "svg",
attributes: {},
children: [
{ type: "element", name: "rect", attributes: {}, children: [] },
],
});
expect(result).toBe("<svg><rect/></svg>");
});
it("round-trips parse → serialize for complex SVG", {
timeout: 5000,
}, () => {
const input =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g id="layer1"><circle cx="50" cy="50" r="40" fill="red"/><text x="10" y="90">label</text></g></svg>';
const node = parseSvg(input);
const output = serializeSvg(node);
// Re-parse the output and check structure
const reparsed = parseSvg(output);
expect(reparsed.name).toBe("svg");
expect(reparsed.children).toHaveLength(1);
const g = reparsed.children![0]!;
expect(g.name).toBe("g");
expect(g.children).toHaveLength(2);
});
it("serializes a text-type node directly", { timeout: 5000 }, () => {
const textNode = { type: "text" as const, value: "hello" };
expect(serializeSvg(textNode)).toBe("hello");
});
it("serializes a doctype node", { timeout: 5000 }, () => {
const doctype = {
type: "doctype" as const,
value: 'svg PUBLIC "-//W3C//DTD SVG 1.1//EN"',
};
expect(serializeSvg(doctype)).toBe(
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN">'
);
});
it("omits undefined attributes", { timeout: 5000 }, () => {
const node = serializeSvg({
type: "element",
name: "svg",
children: [],
});
expect(node).toBe("<svg/>");
});
});