@cyberalien/svg-utils 0.0.1

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.
package/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { ParsedXMLNode, ParsedXMLTagElement, ParsedXMLTextElement } from "./xml/types.js";
2
+ import { iterateXMLContent } from "./xml/iterate.js";
3
+ import { parseXMLContent } from "./xml/parse.js";
4
+ import { stringifyXMLContent } from "./xml/stringify.js";
5
+ export { ParsedXMLNode, ParsedXMLTagElement, ParsedXMLTextElement, iterateXMLContent, parseXMLContent, stringifyXMLContent };
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { iterateXMLContent } from "./xml/iterate.js";
2
+ import { parseXMLContent } from "./xml/parse.js";
3
+ import { stringifyXMLContent } from "./xml/stringify.js";
4
+
5
+ export { iterateXMLContent, parseXMLContent, stringifyXMLContent };
@@ -0,0 +1,12 @@
1
+ import { ParsedXMLNode, ParsedXMLTagElement } from "./types.js";
2
+ /**
3
+ * Callback result
4
+ *
5
+ * - void: continue
6
+ * - 'remove': remove current node
7
+ * - 'skip': skip children
8
+ * - 'abort': stop iteration
9
+ */
10
+ type CallbackResult = void | 'remove' | 'skip' | 'abort';
11
+ declare function iterateXMLContent(root: ParsedXMLTagElement[], callback: (node: ParsedXMLNode, stack: ParsedXMLTagElement[]) => CallbackResult): ParsedXMLNode[];
12
+ export { iterateXMLContent };
@@ -0,0 +1,26 @@
1
+ function iterateXMLContent(root, callback) {
2
+ const stack = [];
3
+ let aborted = false;
4
+ function parseChildren(nodes) {
5
+ const remove = [];
6
+ for (const node of nodes) if (parse(node) === "remove") remove.push(node);
7
+ return remove.length ? nodes.filter((item) => !remove.includes(item)) : nodes;
8
+ }
9
+ function parse(node) {
10
+ if (aborted) return;
11
+ const result = callback(node, stack);
12
+ switch (result) {
13
+ case "abort": aborted = true;
14
+ case "remove":
15
+ case "skip": return result;
16
+ }
17
+ if (node.type === "tag") {
18
+ stack.push(node);
19
+ node.children = parseChildren(node.children);
20
+ stack.pop();
21
+ }
22
+ }
23
+ return parseChildren(root);
24
+ }
25
+
26
+ export { iterateXMLContent };
@@ -0,0 +1,8 @@
1
+ import { ParsedXMLTagElement } from "./types.js";
2
+ /**
3
+ * Parse SVG content
4
+ *
5
+ * Returns null on error
6
+ */
7
+ declare function parseXMLContent(content: string, trim?: boolean): ParsedXMLTagElement[] | null;
8
+ export { parseXMLContent };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Parse SVG content
3
+ *
4
+ * Returns null on error
5
+ */
6
+ function parseXMLContent(content, trim = true) {
7
+ const stack = [];
8
+ const rootNodes = [];
9
+ let startIndex = 0;
10
+ let parentNode = null;
11
+ do {
12
+ const start = content.indexOf("<", startIndex);
13
+ const end = start === -1 ? -1 : content.indexOf(">", start);
14
+ if (start === -1 || end === -1) {
15
+ const text$1 = content.slice(startIndex).trim();
16
+ if (text$1 || parentNode || !rootNodes.length) return null;
17
+ return rootNodes;
18
+ }
19
+ if (content.slice(start, start + 4) === "<!--") {
20
+ const end$1 = content.indexOf("-->", start);
21
+ if (end$1 === -1) return null;
22
+ startIndex = end$1 + 3;
23
+ continue;
24
+ }
25
+ const rawText = content.slice(startIndex, start);
26
+ const text = trim ? rawText.trim() : rawText;
27
+ startIndex = start;
28
+ if (text) {
29
+ if (!parentNode) return null;
30
+ parentNode.children.push({
31
+ type: "text",
32
+ content: text
33
+ });
34
+ }
35
+ let tagContent = content.slice(start + 1, end).trim();
36
+ if (tagContent.startsWith("/")) {
37
+ if (!parentNode) return null;
38
+ const tagNameMatch$1 = tagContent.slice(1).match(/^[^\s]+/);
39
+ if (parentNode.tag !== tagNameMatch$1?.[0]) return null;
40
+ stack.pop();
41
+ parentNode = stack.length ? stack[stack.length - 1] : null;
42
+ startIndex = end + 1;
43
+ continue;
44
+ }
45
+ const tagNameMatch = tagContent.match(/^[^\s/]+/);
46
+ if (!tagNameMatch) return null;
47
+ const tagName = tagNameMatch[0];
48
+ tagContent = tagContent.slice(tagName.length).trim();
49
+ const selfClosing = tagContent.slice(-1) === "/";
50
+ if (selfClosing) tagContent = tagContent.slice(0, -1).trim();
51
+ const attribs = Object.create(null);
52
+ Array.from(tagContent.matchAll(/([\w:-]+)="([^"]*)"/g) ?? []).forEach((match) => {
53
+ attribs[match[1]] = match[2];
54
+ });
55
+ const element = {
56
+ type: "tag",
57
+ tag: tagName,
58
+ attribs,
59
+ children: []
60
+ };
61
+ if (parentNode) parentNode.children.push(element);
62
+ else rootNodes.push(element);
63
+ if (!selfClosing) {
64
+ stack.push(element);
65
+ parentNode = element;
66
+ }
67
+ startIndex = end + 1;
68
+ if (tagName === "style" && !selfClosing) {
69
+ const match = "</style>";
70
+ const end$1 = content.indexOf(match, startIndex);
71
+ if (end$1 === -1) return null;
72
+ const css = content.slice(startIndex, end$1).trim();
73
+ if (css.length) parentNode.children.push({
74
+ type: "text",
75
+ content: css
76
+ });
77
+ stack.pop();
78
+ parentNode = stack.length ? stack[stack.length - 1] : null;
79
+ startIndex = end$1 + 8;
80
+ }
81
+ } while (true);
82
+ }
83
+
84
+ export { parseXMLContent };
@@ -0,0 +1,10 @@
1
+ import { ParsedXMLTagElement } from "./types.js";
2
+ interface ParseSVGOptions {
3
+ useSelfClosing?: boolean;
4
+ numberTemplate?: string;
5
+ }
6
+ /**
7
+ * Convert parsed XML content to string
8
+ */
9
+ declare function stringifyXMLContent(root: ParsedXMLTagElement[], options?: ParseSVGOptions): string | null;
10
+ export { stringifyXMLContent };
@@ -0,0 +1,53 @@
1
+ const defaultOptions = {
2
+ useSelfClosing: true,
3
+ numberTemplate: ` {key}="{value}"`
4
+ };
5
+ function assertNever(v) {}
6
+ /**
7
+ * Convert parsed XML content to string
8
+ */
9
+ function stringifyXMLContent(root, options) {
10
+ const fullOptions = {
11
+ ...defaultOptions,
12
+ ...options
13
+ };
14
+ let output = "";
15
+ const add = (node) => {
16
+ output += "<" + node.tag;
17
+ for (const key in node.attribs) {
18
+ const value = node.attribs[key];
19
+ switch (typeof value) {
20
+ case "string":
21
+ output += ` ${key}="${value}"`;
22
+ break;
23
+ case "number":
24
+ output += fullOptions.numberTemplate.replace("{value}", value.toString()).replace("{key}", key);
25
+ break;
26
+ }
27
+ }
28
+ if (!node.children.length) {
29
+ if (fullOptions.useSelfClosing) output += " />";
30
+ else output += "></" + node.tag + ">";
31
+ return true;
32
+ }
33
+ output += ">";
34
+ for (let i = 0; i < node.children.length; i++) {
35
+ const childNode = node.children[i];
36
+ switch (childNode.type) {
37
+ case "tag":
38
+ if (!add(childNode)) return false;
39
+ break;
40
+ case "text":
41
+ output += childNode.content;
42
+ break;
43
+ default: assertNever(childNode);
44
+ }
45
+ }
46
+ output += "</" + node.tag + ">";
47
+ return true;
48
+ };
49
+ for (const node of root) if (!add(node)) return null;
50
+ return output;
51
+ }
52
+
53
+ export { stringifyXMLContent };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Tag
3
+ */
4
+ interface ParsedXMLTagElement {
5
+ type: 'tag';
6
+ tag: string;
7
+ attribs: Record<string, string | number>;
8
+ children: ParsedXMLNode[];
9
+ }
10
+ /**
11
+ * Text
12
+ */
13
+ interface ParsedXMLTextElement {
14
+ type: 'text';
15
+ content: string;
16
+ }
17
+ /**
18
+ * Element in tree
19
+ */
20
+ type ParsedXMLNode = ParsedXMLTagElement | ParsedXMLTextElement;
21
+ export { ParsedXMLNode, ParsedXMLTagElement, ParsedXMLTextElement };
File without changes
package/license.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT Vjacheslav Trushkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cyberalien/svg-utils",
3
+ "type": "module",
4
+ "description": "Common functions for working with SVG used by various packages.",
5
+ "author": "Vjacheslav Trushkin",
6
+ "version": "0.0.1",
7
+ "license": "MIT",
8
+ "bugs": "https://github.com/cyberalien/svg-utils/issues",
9
+ "homepage": "https://cyberalien.dev/",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/cyberalien/svg-utils.git"
13
+ },
14
+ "sideEffects": false,
15
+ "main": "lib/index.js",
16
+ "module": "lib/index.js",
17
+ "types": "lib/index.d.ts",
18
+ "files": [
19
+ "lib",
20
+ "*.d.ts"
21
+ ],
22
+ "dependencies": {
23
+ "@iconify/types": "^2.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/eslintrc": "^3.3.1",
27
+ "@eslint/js": "^9.31.0",
28
+ "@types/jest": "^29.5.14",
29
+ "@types/node": "^18.19.120",
30
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
31
+ "eslint": "^9.31.0",
32
+ "globals": "^16.3.0",
33
+ "tsdown": "^0.13.0",
34
+ "typescript": "^5.8.3",
35
+ "vitest": "^2.1.9"
36
+ },
37
+ "scripts": {
38
+ "lint": "eslint --fix src/**/*.ts",
39
+ "prebuild": "pnpm run lint",
40
+ "build": "tsdown",
41
+ "test": "vitest"
42
+ }
43
+ }