Skip to content

Commit bb3f1a9

Browse files
committed
[new plugin] reimplement inlineDefs
The code is taken from #976, refactored with new api, covered types and simplified. Plugin has no dependencies so can be used without changing. ``` const inlineDefs = require('./inlineDefs.js'); module.exports = { plugins: [ 'preset-default', inlineDefs ] }; ```
1 parent 238d3bf commit bb3f1a9

16 files changed

+403
-0
lines changed

plugins/inlineDefs.js

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
'use strict';
2+
3+
/**
4+
* @typedef {import('../lib/types').XastParent} XastParent
5+
* @typedef {import('../lib/types').XastElement} XastElement
6+
*/
7+
8+
exports.name = 'inlineDefs';
9+
exports.type = 'visitor';
10+
exports.active = true;
11+
exports.description = 'inlines svg definitions';
12+
13+
/**
14+
* @typedef {(element: XastElement, parentNode: XastParent) => void} VisitCallback
15+
*/
16+
17+
/**
18+
* @type {(element: XastParent, fn: VisitCallback) => void}
19+
*/
20+
const visitElements = (node, fn) => {
21+
for (const child of node.children) {
22+
if (child.type === 'element') {
23+
fn(child, node);
24+
visitElements(child, fn);
25+
}
26+
}
27+
};
28+
29+
/**
30+
* Replaces use tag with the corresponding definitions
31+
* if onlyUnique is enabled, replaces only use tags with definitions referred to only once
32+
*
33+
* @type {import('../lib/types').Plugin<{
34+
* onlyUnique?: boolean
35+
* }>}
36+
*/
37+
exports.fn = (root, params) => {
38+
const { onlyUnique = true } = params;
39+
// hacky extract JSAPI class to avoid imports from other modules
40+
const JSAPI = root.constructor;
41+
42+
/**
43+
* @type {[XastElement, XastParent][]}
44+
*/
45+
const uses = [];
46+
/**
47+
* @type {Map<string, number>}
48+
*/
49+
const useCounts = new Map();
50+
/**
51+
* @type {Map<string, XastElement>}
52+
*/
53+
const referencedElements = new Map();
54+
55+
// collect defs container and all uses
56+
visitElements(root, (node, parentNode) => {
57+
if (node.name === 'use') {
58+
uses.push([node, parentNode]);
59+
const href = node.attributes['xlink:href'] || node.attributes.href;
60+
const count = useCounts.get(href) || 0;
61+
useCounts.set(href, count + 1);
62+
}
63+
});
64+
65+
return {
66+
element: {
67+
enter: (node, parentNode) => {
68+
// find elements referenced by all <use>
69+
if (node.attributes.id == null) {
70+
return;
71+
}
72+
const href = `#${node.attributes.id}`;
73+
const count = useCounts.get(href);
74+
// not referenced
75+
if (count == null) {
76+
return;
77+
}
78+
referencedElements.set(href, node);
79+
/// remove id attribute when referenced yb <use> more than once
80+
if (onlyUnique === false && count > 1) {
81+
delete node.attributes.id;
82+
}
83+
// remove elements referenced by <use> only once
84+
if (onlyUnique === true && count === 1) {
85+
parentNode.children = parentNode.children.filter(
86+
(child) => child !== node
87+
);
88+
}
89+
},
90+
91+
exit(node, parentNode) {
92+
// remove empty <defs> container
93+
if (node.name === 'defs') {
94+
if (onlyUnique === false || node.children.length === 0) {
95+
parentNode.children = parentNode.children.filter(
96+
(child) => child !== node
97+
);
98+
}
99+
}
100+
},
101+
},
102+
103+
root: {
104+
exit: () => {
105+
for (const [use, useParentNode] of uses) {
106+
const href = use.attributes['xlink:href'] || use.attributes['href'];
107+
const count = useCounts.get(href) || 0;
108+
const referenced = referencedElements.get(href);
109+
110+
if (onlyUnique === true && count > 1) {
111+
continue;
112+
}
113+
if (referenced == null) {
114+
continue;
115+
}
116+
117+
// copy attrubutes from <use> to referenced element
118+
for (const [name, value] of Object.entries(use.attributes)) {
119+
if (
120+
name !== 'x' &&
121+
name !== 'y' &&
122+
name !== 'xlink:href' &&
123+
name !== 'href'
124+
) {
125+
referenced.attributes[name] = value;
126+
}
127+
}
128+
129+
const x = use.attributes.x;
130+
const y = use.attributes.y;
131+
let attrValue = null;
132+
if (x != null && y != null) {
133+
attrValue = `translate(${x}, ${y})`;
134+
} else if (x != null) {
135+
attrValue = `translate(${x})`;
136+
}
137+
138+
let replacement = referenced;
139+
// wrap referenced element with <g> when <use> had coordinates
140+
if (attrValue != null) {
141+
/**
142+
* @type {XastElement}
143+
*/
144+
const g = {
145+
type: 'element',
146+
name: 'g',
147+
attributes: {
148+
transform: attrValue,
149+
},
150+
children: [referenced],
151+
};
152+
// @ts-ignore
153+
replacement = new JSAPI(g);
154+
}
155+
useParentNode.children = useParentNode.children.map((child) => {
156+
if (child === use) {
157+
return replacement;
158+
} else {
159+
return child;
160+
}
161+
});
162+
}
163+
},
164+
},
165+
};
166+
};

plugins/plugins.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ exports.removeXMLProcInst = require('./removeXMLProcInst.js');
5454
exports.reusePaths = require('./reusePaths.js');
5555
exports.sortAttrs = require('./sortAttrs.js');
5656
exports.sortDefsChildren = require('./sortDefsChildren.js');
57+
exports.inlineDefs = require('./inlineDefs.js');

test/plugins/inlineDefs.01.svg

+12
Loading

test/plugins/inlineDefs.02.svg

+20
Loading

test/plugins/inlineDefs.03.svg

+15
Loading

test/plugins/inlineDefs.04.svg

+14
Loading

test/plugins/inlineDefs.05.svg

+17
Loading

test/plugins/inlineDefs.06.svg

+22
Loading

test/plugins/inlineDefs.07.svg

+18
Loading

test/plugins/inlineDefs.08.svg

+12
Loading

test/plugins/inlineDefs.09.svg

+20
Loading

test/plugins/inlineDefs.10.svg

+15
Loading

test/plugins/inlineDefs.11.svg

+14
Loading

test/plugins/inlineDefs.12.svg

+17
Loading

0 commit comments

Comments
 (0)