@cyberalien/svg-utils 0.0.2 → 0.0.4
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/svg/ids.d.ts +7 -0
- package/lib/svg/ids.js +107 -0
- package/lib/xml/stringify.d.ts +1 -0
- package/lib/xml/stringify.js +14 -9
- package/package.json +1 -1
package/lib/svg/ids.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ParsedXMLTagElement } from "../xml/types.js";
|
|
2
|
+
type HashCallback = (id: string, content: string, tagName: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Change IDs in SVG using a callback function
|
|
5
|
+
*/
|
|
6
|
+
declare function changeSVGIDs(root: ParsedXMLTagElement[], callback: HashCallback): void;
|
|
7
|
+
export { changeSVGIDs };
|
package/lib/svg/ids.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { cloneObject } from "../helpers/misc/clone.js";
|
|
2
|
+
import { iterateXMLContent } from "../xml/iterate.js";
|
|
3
|
+
import { stringifyXMLContent } from "../xml/stringify.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Change IDs in SVG using a callback function
|
|
7
|
+
*/
|
|
8
|
+
function changeSVGIDs(root, callback) {
|
|
9
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
10
|
+
const idNodes = /* @__PURE__ */ new Map();
|
|
11
|
+
const nestedIDs = /* @__PURE__ */ new Map();
|
|
12
|
+
const usage = [];
|
|
13
|
+
const parse = (replacement) => {
|
|
14
|
+
iterateXMLContent(root, (node, stack) => {
|
|
15
|
+
if (node.type !== "tag") return;
|
|
16
|
+
const attribs = node.attribs;
|
|
17
|
+
const nodeID = attribs.id;
|
|
18
|
+
if (typeof nodeID === "string") {
|
|
19
|
+
if (!replacement) {
|
|
20
|
+
if (idNodes.has(nodeID)) throw new Error(`Duplicate ID found: ${nodeID}`);
|
|
21
|
+
idNodes.set(nodeID, node);
|
|
22
|
+
idMap.set(node, nodeID);
|
|
23
|
+
} else if (nodeID === replacement[0]) attribs.id = replacement[1];
|
|
24
|
+
}
|
|
25
|
+
for (const attrib in attribs) {
|
|
26
|
+
const value = attribs[attrib];
|
|
27
|
+
if (typeof value !== "string") continue;
|
|
28
|
+
const add = (id) => {
|
|
29
|
+
usage.push({
|
|
30
|
+
node,
|
|
31
|
+
attrib,
|
|
32
|
+
id
|
|
33
|
+
});
|
|
34
|
+
[node, ...stack].forEach((node$1) => {
|
|
35
|
+
const parentID = idMap.get(node$1);
|
|
36
|
+
if (parentID) {
|
|
37
|
+
const nested = nestedIDs.get(parentID) || [];
|
|
38
|
+
if (!nested.includes(id)) {
|
|
39
|
+
nested.push(id);
|
|
40
|
+
nestedIDs.set(parentID, nested);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
switch (attrib) {
|
|
46
|
+
case "id": break;
|
|
47
|
+
case "begin":
|
|
48
|
+
case "end": {
|
|
49
|
+
const newValue = value.split(";").map((part) => {
|
|
50
|
+
const chunks = part.trim().split(".");
|
|
51
|
+
if (chunks.length < 2) return part;
|
|
52
|
+
const time = chunks[1];
|
|
53
|
+
if (time?.startsWith("begin") || time?.startsWith("end")) {
|
|
54
|
+
const id = chunks.shift();
|
|
55
|
+
if (!replacement) add(id);
|
|
56
|
+
else if (id === replacement[0]) return `${replacement[1]}.${chunks.join(".")}`;
|
|
57
|
+
}
|
|
58
|
+
return part;
|
|
59
|
+
}).join(";");
|
|
60
|
+
if (replacement) attribs[attrib] = newValue;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "href":
|
|
64
|
+
case "xlink:href":
|
|
65
|
+
if (value.startsWith("#")) {
|
|
66
|
+
const id = value.slice(1);
|
|
67
|
+
if (!replacement) add(id);
|
|
68
|
+
else if (id === replacement[0]) attribs[attrib] = `#${replacement[1]}`;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
default: if (value.startsWith("url(#")) {
|
|
72
|
+
const id = value.slice(5, -1);
|
|
73
|
+
if (!replacement) add(id);
|
|
74
|
+
else if (id === replacement[0]) attribs[attrib] = `url(#${replacement[1]})`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
parse();
|
|
81
|
+
if (!idMap.size) return;
|
|
82
|
+
const allIDs = new Set(idMap.values());
|
|
83
|
+
const parseIDs = (parseAll = false) => {
|
|
84
|
+
const oldSize = allIDs.size;
|
|
85
|
+
for (const id of allIDs) {
|
|
86
|
+
const nested = nestedIDs.get(id)?.filter((nestedID) => nestedID !== id && allIDs.has(nestedID)) ?? [];
|
|
87
|
+
if (parseAll || !nested.length) {
|
|
88
|
+
const node = idNodes.get(id);
|
|
89
|
+
const newNode = cloneObject(node);
|
|
90
|
+
delete newNode.attribs.id;
|
|
91
|
+
for (const usageItem of usage) if (usageItem.node === node && usageItem.id === id) delete newNode.attribs[usageItem.attrib];
|
|
92
|
+
const content = stringifyXMLContent([newNode]);
|
|
93
|
+
if (!content) throw new Error(`Failed to stringify node with ID: ${id}`);
|
|
94
|
+
const newID = callback(id, content, node.tag);
|
|
95
|
+
if (newID !== id) parse([id, newID]);
|
|
96
|
+
allIDs.delete(id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return allIDs.size !== oldSize;
|
|
100
|
+
};
|
|
101
|
+
while (allIDs.size) if (!parseIDs()) {
|
|
102
|
+
parseIDs(true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { changeSVGIDs };
|
package/lib/xml/stringify.d.ts
CHANGED
package/lib/xml/stringify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const defaultOptions = {
|
|
2
2
|
useSelfClosing: true,
|
|
3
|
-
numberTemplate: ` {key}="{value}"
|
|
3
|
+
numberTemplate: ` {key}="{value}"`,
|
|
4
|
+
prettyPrint: false
|
|
4
5
|
};
|
|
5
6
|
function assertNever(v) {}
|
|
6
7
|
/**
|
|
@@ -11,9 +12,13 @@ function stringifyXMLContent(root, options) {
|
|
|
11
12
|
...defaultOptions,
|
|
12
13
|
...options
|
|
13
14
|
};
|
|
15
|
+
const { prettyPrint } = fullOptions;
|
|
14
16
|
let output = "";
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
const tab = typeof prettyPrint === "string" ? prettyPrint : prettyPrint ? " " : "";
|
|
18
|
+
const tabs = (length) => tab.repeat(length);
|
|
19
|
+
const nl = prettyPrint === false ? "" : "\n";
|
|
20
|
+
const add = (node, depth) => {
|
|
21
|
+
output += tabs(depth) + "<" + node.tag;
|
|
17
22
|
for (const key in node.attribs) {
|
|
18
23
|
const value = node.attribs[key];
|
|
19
24
|
switch (typeof value) {
|
|
@@ -26,16 +31,16 @@ function stringifyXMLContent(root, options) {
|
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
if (!node.children.length) {
|
|
29
|
-
if (fullOptions.useSelfClosing) output += " />";
|
|
30
|
-
else output += "></" + node.tag + ">";
|
|
34
|
+
if (fullOptions.useSelfClosing) output += " />" + nl;
|
|
35
|
+
else output += "></" + node.tag + ">" + nl;
|
|
31
36
|
return true;
|
|
32
37
|
}
|
|
33
|
-
output += ">";
|
|
38
|
+
output += ">" + nl;
|
|
34
39
|
for (let i = 0; i < node.children.length; i++) {
|
|
35
40
|
const childNode = node.children[i];
|
|
36
41
|
switch (childNode.type) {
|
|
37
42
|
case "tag":
|
|
38
|
-
if (!add(childNode)) return false;
|
|
43
|
+
if (!add(childNode, depth + 1)) return false;
|
|
39
44
|
break;
|
|
40
45
|
case "text":
|
|
41
46
|
output += childNode.content;
|
|
@@ -43,10 +48,10 @@ function stringifyXMLContent(root, options) {
|
|
|
43
48
|
default: assertNever(childNode);
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
|
-
output += "</" + node.tag + ">";
|
|
51
|
+
output += tabs(depth) + "</" + node.tag + ">" + nl;
|
|
47
52
|
return true;
|
|
48
53
|
};
|
|
49
|
-
for (const node of root) if (!add(node)) return null;
|
|
54
|
+
for (const node of root) if (!add(node, 0)) return null;
|
|
50
55
|
return output;
|
|
51
56
|
}
|
|
52
57
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Common functions for working with SVG used by various packages.",
|
|
5
5
|
"author": "Vjacheslav Trushkin",
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.4",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"bugs": "https://github.com/cyberalien/svg-utils/issues",
|
|
9
9
|
"homepage": "https://cyberalien.dev/",
|