1 | 'use strict';
|
---|
2 |
|
---|
3 | /**
|
---|
4 | * @typedef {import('../lib/types').PathDataItem} PathDataItem
|
---|
5 | */
|
---|
6 |
|
---|
7 | const { stringifyPathData } = require('../lib/path.js');
|
---|
8 | const { detachNodeFromParent } = require('../lib/xast.js');
|
---|
9 |
|
---|
10 | exports.name = 'convertShapeToPath';
|
---|
11 | exports.type = 'visitor';
|
---|
12 | exports.active = true;
|
---|
13 | exports.description = 'converts basic shapes to more compact path form';
|
---|
14 |
|
---|
15 | const regNumber = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
|
---|
16 |
|
---|
17 | /**
|
---|
18 | * Converts basic shape to more compact path.
|
---|
19 | * It also allows further optimizations like
|
---|
20 | * combining paths with similar attributes.
|
---|
21 | *
|
---|
22 | * @see https://www.w3.org/TR/SVG11/shapes.html
|
---|
23 | *
|
---|
24 | * @author Lev Solntsev
|
---|
25 | *
|
---|
26 | * @type {import('../lib/types').Plugin<{
|
---|
27 | * convertArcs?: boolean,
|
---|
28 | * floatPrecision?: number
|
---|
29 | * }>}
|
---|
30 | */
|
---|
31 | exports.fn = (root, params) => {
|
---|
32 | const { convertArcs = false, floatPrecision: precision } = params;
|
---|
33 |
|
---|
34 | return {
|
---|
35 | element: {
|
---|
36 | enter: (node, parentNode) => {
|
---|
37 | // convert rect to path
|
---|
38 | if (
|
---|
39 | node.name === 'rect' &&
|
---|
40 | node.attributes.width != null &&
|
---|
41 | node.attributes.height != null &&
|
---|
42 | node.attributes.rx == null &&
|
---|
43 | node.attributes.ry == null
|
---|
44 | ) {
|
---|
45 | const x = Number(node.attributes.x || '0');
|
---|
46 | const y = Number(node.attributes.y || '0');
|
---|
47 | const width = Number(node.attributes.width);
|
---|
48 | const height = Number(node.attributes.height);
|
---|
49 | // Values like '100%' compute to NaN, thus running after
|
---|
50 | // cleanupNumericValues when 'px' units has already been removed.
|
---|
51 | // TODO: Calculate sizes from % and non-px units if possible.
|
---|
52 | if (Number.isNaN(x - y + width - height)) return;
|
---|
53 | /**
|
---|
54 | * @type {Array<PathDataItem>}
|
---|
55 | */
|
---|
56 | const pathData = [
|
---|
57 | { command: 'M', args: [x, y] },
|
---|
58 | { command: 'H', args: [x + width] },
|
---|
59 | { command: 'V', args: [y + height] },
|
---|
60 | { command: 'H', args: [x] },
|
---|
61 | { command: 'z', args: [] },
|
---|
62 | ];
|
---|
63 | node.name = 'path';
|
---|
64 | node.attributes.d = stringifyPathData({ pathData, precision });
|
---|
65 | delete node.attributes.x;
|
---|
66 | delete node.attributes.y;
|
---|
67 | delete node.attributes.width;
|
---|
68 | delete node.attributes.height;
|
---|
69 | }
|
---|
70 |
|
---|
71 | // convert line to path
|
---|
72 | if (node.name === 'line') {
|
---|
73 | const x1 = Number(node.attributes.x1 || '0');
|
---|
74 | const y1 = Number(node.attributes.y1 || '0');
|
---|
75 | const x2 = Number(node.attributes.x2 || '0');
|
---|
76 | const y2 = Number(node.attributes.y2 || '0');
|
---|
77 | if (Number.isNaN(x1 - y1 + x2 - y2)) return;
|
---|
78 | /**
|
---|
79 | * @type {Array<PathDataItem>}
|
---|
80 | */
|
---|
81 | const pathData = [
|
---|
82 | { command: 'M', args: [x1, y1] },
|
---|
83 | { command: 'L', args: [x2, y2] },
|
---|
84 | ];
|
---|
85 | node.name = 'path';
|
---|
86 | node.attributes.d = stringifyPathData({ pathData, precision });
|
---|
87 | delete node.attributes.x1;
|
---|
88 | delete node.attributes.y1;
|
---|
89 | delete node.attributes.x2;
|
---|
90 | delete node.attributes.y2;
|
---|
91 | }
|
---|
92 |
|
---|
93 | // convert polyline and polygon to path
|
---|
94 | if (
|
---|
95 | (node.name === 'polyline' || node.name === 'polygon') &&
|
---|
96 | node.attributes.points != null
|
---|
97 | ) {
|
---|
98 | const coords = (node.attributes.points.match(regNumber) || []).map(
|
---|
99 | Number
|
---|
100 | );
|
---|
101 | if (coords.length < 4) {
|
---|
102 | detachNodeFromParent(node, parentNode);
|
---|
103 | return;
|
---|
104 | }
|
---|
105 | /**
|
---|
106 | * @type {Array<PathDataItem>}
|
---|
107 | */
|
---|
108 | const pathData = [];
|
---|
109 | for (let i = 0; i < coords.length; i += 2) {
|
---|
110 | pathData.push({
|
---|
111 | command: i === 0 ? 'M' : 'L',
|
---|
112 | args: coords.slice(i, i + 2),
|
---|
113 | });
|
---|
114 | }
|
---|
115 | if (node.name === 'polygon') {
|
---|
116 | pathData.push({ command: 'z', args: [] });
|
---|
117 | }
|
---|
118 | node.name = 'path';
|
---|
119 | node.attributes.d = stringifyPathData({ pathData, precision });
|
---|
120 | delete node.attributes.points;
|
---|
121 | }
|
---|
122 |
|
---|
123 | // optionally convert circle
|
---|
124 | if (node.name === 'circle' && convertArcs) {
|
---|
125 | const cx = Number(node.attributes.cx || '0');
|
---|
126 | const cy = Number(node.attributes.cy || '0');
|
---|
127 | const r = Number(node.attributes.r || '0');
|
---|
128 | if (Number.isNaN(cx - cy + r)) {
|
---|
129 | return;
|
---|
130 | }
|
---|
131 | /**
|
---|
132 | * @type {Array<PathDataItem>}
|
---|
133 | */
|
---|
134 | const pathData = [
|
---|
135 | { command: 'M', args: [cx, cy - r] },
|
---|
136 | { command: 'A', args: [r, r, 0, 1, 0, cx, cy + r] },
|
---|
137 | { command: 'A', args: [r, r, 0, 1, 0, cx, cy - r] },
|
---|
138 | { command: 'z', args: [] },
|
---|
139 | ];
|
---|
140 | node.name = 'path';
|
---|
141 | node.attributes.d = stringifyPathData({ pathData, precision });
|
---|
142 | delete node.attributes.cx;
|
---|
143 | delete node.attributes.cy;
|
---|
144 | delete node.attributes.r;
|
---|
145 | }
|
---|
146 |
|
---|
147 | // optionally covert ellipse
|
---|
148 | if (node.name === 'ellipse' && convertArcs) {
|
---|
149 | const ecx = Number(node.attributes.cx || '0');
|
---|
150 | const ecy = Number(node.attributes.cy || '0');
|
---|
151 | const rx = Number(node.attributes.rx || '0');
|
---|
152 | const ry = Number(node.attributes.ry || '0');
|
---|
153 | if (Number.isNaN(ecx - ecy + rx - ry)) {
|
---|
154 | return;
|
---|
155 | }
|
---|
156 | /**
|
---|
157 | * @type {Array<PathDataItem>}
|
---|
158 | */
|
---|
159 | const pathData = [
|
---|
160 | { command: 'M', args: [ecx, ecy - ry] },
|
---|
161 | { command: 'A', args: [rx, ry, 0, 1, 0, ecx, ecy + ry] },
|
---|
162 | { command: 'A', args: [rx, ry, 0, 1, 0, ecx, ecy - ry] },
|
---|
163 | { command: 'z', args: [] },
|
---|
164 | ];
|
---|
165 | node.name = 'path';
|
---|
166 | node.attributes.d = stringifyPathData({ pathData, precision });
|
---|
167 | delete node.attributes.cx;
|
---|
168 | delete node.attributes.cy;
|
---|
169 | delete node.attributes.rx;
|
---|
170 | delete node.attributes.ry;
|
---|
171 | }
|
---|
172 | },
|
---|
173 | },
|
---|
174 | };
|
---|
175 | };
|
---|