svg-path-commander 2.0.3 → 2.0.5

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svg-path-commander",
3
3
  "author": "thednp",
4
4
  "license": "MIT",
5
- "version": "2.0.3",
5
+ "version": "2.0.5",
6
6
  "description": "Modern TypeScript tools for SVG",
7
7
  "source": "./src/index.ts",
8
8
  "main": "./dist/svg-path-commander.js",
@@ -73,6 +73,6 @@
73
73
  "vite": "^4.2.0"
74
74
  },
75
75
  "dependencies": {
76
- "@thednp/dommatrix": "^2.0.2"
76
+ "@thednp/dommatrix": "^2.0.4"
77
77
  }
78
78
  }
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import isAbsoluteArray from './util/isAbsoluteArray';
29
29
  import isRelativeArray from './util/isRelativeArray';
30
30
  import isCurveArray from './util/isCurveArray';
31
31
  import isNormalizedArray from './util/isNormalizedArray';
32
+ import shapeToPathArray from './util/shapeToPathArray';
32
33
  import shapeToPath from './util/shapeToPath';
33
34
 
34
35
  import roundPath from './process/roundPath';
@@ -79,6 +80,7 @@ class SVGPathCommander {
79
80
  public static isCurveArray = isCurveArray;
80
81
  public static isNormalizedArray = isNormalizedArray;
81
82
  public static shapeToPath = shapeToPath;
83
+ public static shapeToPathArray = shapeToPathArray;
82
84
  public static parsePathString = parsePathString;
83
85
  public static roundPath = roundPath;
84
86
  public static splitPath = splitPath;
package/src/interface.ts CHANGED
@@ -42,8 +42,8 @@ export interface EllipseAttr {
42
42
  cx: number;
43
43
  cy: number;
44
44
  rx: number;
45
- ry: number;
46
- [key: string]: string | number;
45
+ ry?: number;
46
+ [key: string]: string | number | undefined;
47
47
  }
48
48
  export interface RectAttr {
49
49
  type: 'rect';
@@ -51,9 +51,9 @@ export interface RectAttr {
51
51
  height: number;
52
52
  x: number;
53
53
  y: number;
54
- rx: number;
55
- ry: number;
56
- [key: string]: string | number;
54
+ rx?: number;
55
+ ry?: number;
56
+ [key: string]: string | number | undefined;
57
57
  }
58
58
  export interface GlyphAttr {
59
59
  type: 'glyph';
@@ -12,8 +12,13 @@ const isPathArray = (path: unknown): path is PathArray => {
12
12
  Array.isArray(path) &&
13
13
  path.every((seg: PathSegment) => {
14
14
  const lk = seg[0].toLowerCase() as RelativeCommand;
15
- return paramsCount[lk] === seg.length - 1 && 'achlmqstvz'.includes(lk);
16
- })
15
+ return (
16
+ paramsCount[lk] === seg.length - 1 &&
17
+ 'achlmqstvz'.includes(lk) &&
18
+ (seg.slice(1) as unknown[]).every(Number.isFinite)
19
+ );
20
+ }) &&
21
+ path.length > 0
17
22
  );
18
23
  };
19
24
  export default isPathArray;
@@ -10,7 +10,7 @@ import PathParser from '../parser/pathParser';
10
10
  * @returns the path string validity
11
11
  */
12
12
  const isValidPath = (pathString: string) => {
13
- if (typeof pathString !== 'string') {
13
+ if (typeof pathString !== 'string' || !pathString.length) {
14
14
  return false;
15
15
  }
16
16
 
@@ -0,0 +1,16 @@
1
+ import type { ShapeParams } from '../interface';
2
+
3
+ /**
4
+ * Supported shapes and their specific parameters.
5
+ */
6
+ const shapeParams: ShapeParams = {
7
+ line: ['x1', 'y1', 'x2', 'y2'],
8
+ circle: ['cx', 'cy', 'r'],
9
+ ellipse: ['cx', 'cy', 'rx', 'ry'],
10
+ rect: ['width', 'height', 'x', 'y', 'rx', 'ry'],
11
+ polygon: ['points'],
12
+ polyline: ['points'],
13
+ glyph: ['d'],
14
+ };
15
+
16
+ export default shapeParams;
@@ -1,130 +1,11 @@
1
- import type { CircleAttr, EllipseAttr, GlyphAttr, LineAttr, PolyAttr, RectAttr, ShapeParams } from '../interface';
2
- import type { PathArray, PathSegment, ShapeOps, ShapeTypes } from '../types';
1
+ import type { ShapeParams } from '../interface';
2
+ import type { ShapeOps, ShapeTypes } from '../types';
3
3
  import pathToString from '../convert/pathToString';
4
4
  import defaultOptions from '../options/options';
5
5
  import error from '../parser/error';
6
6
  import isValidPath from './isValidPath';
7
-
8
- /**
9
- * Supported shapes and their specific parameters.
10
- */
11
- const shapeParams: ShapeParams = {
12
- line: ['x1', 'y1', 'x2', 'y2'],
13
- circle: ['cx', 'cy', 'r'],
14
- ellipse: ['cx', 'cy', 'rx', 'ry'],
15
- rect: ['width', 'height', 'x', 'y', 'rx', 'ry'],
16
- polygon: ['points'],
17
- polyline: ['points'],
18
- glyph: ['d'],
19
- };
20
-
21
- /**
22
- * Returns a new `pathArray` from line attributes.
23
- *
24
- * @param attr shape configuration
25
- * @returns a new line `pathArray`
26
- */
27
- export const getLinePath = (attr: LineAttr): PathArray => {
28
- const { x1, y1, x2, y2 } = attr;
29
- return [
30
- ['M', x1, y1],
31
- ['L', x2, y2],
32
- ];
33
- };
34
-
35
- /**
36
- * Returns a new `pathArray` like from polyline/polygon attributes.
37
- *
38
- * @param attr shape configuration
39
- * @return a new polygon/polyline `pathArray`
40
- */
41
- export const getPolyPath = (attr: PolyAttr): PathArray => {
42
- const pathArray = [] as PathSegment[];
43
- const points = (attr.points || '')
44
- .trim()
45
- .split(/[\s|,]/)
46
- .map(Number);
47
-
48
- let index = 0;
49
- while (index < points.length) {
50
- pathArray.push([index ? 'L' : 'M', points[index], points[index + 1]]);
51
- index += 2;
52
- }
53
-
54
- return (attr.type === 'polygon' ? [...pathArray, ['z']] : pathArray) as PathArray;
55
- };
56
-
57
- /**
58
- * Returns a new `pathArray` from circle attributes.
59
- *
60
- * @param attr shape configuration
61
- * @return a circle `pathArray`
62
- */
63
- export const getCirclePath = (attr: CircleAttr): PathArray => {
64
- const { cx, cy, r } = attr;
65
-
66
- return [
67
- ['M', cx - r, cy],
68
- ['a', r, r, 0, 1, 0, 2 * r, 0],
69
- ['a', r, r, 0, 1, 0, -2 * r, 0],
70
- ];
71
- };
72
-
73
- /**
74
- * Returns a new `pathArray` from ellipse attributes.
75
- *
76
- * @param attr shape configuration
77
- * @return an ellipse `pathArray`
78
- */
79
- export const getEllipsePath = (attr: EllipseAttr): PathArray => {
80
- const { cx, cy, rx, ry } = attr;
81
-
82
- return [
83
- ['M', cx - rx, cy],
84
- ['a', rx, ry, 0, 1, 0, 2 * rx, 0],
85
- ['a', rx, ry, 0, 1, 0, -2 * rx, 0],
86
- ];
87
- };
88
-
89
- /**
90
- * Returns a new `pathArray` like from rect attributes.
91
- *
92
- * @param attr object with properties above
93
- * @return a new `pathArray` from `<rect>` attributes
94
- */
95
- export const getRectanglePath = (attr: RectAttr): PathArray => {
96
- const x = +attr.x || 0;
97
- const y = +attr.y || 0;
98
- const w = +attr.width;
99
- const h = +attr.height;
100
- let rx = +attr.rx;
101
- let ry = +attr.ry;
102
-
103
- // Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement:
104
- if (rx || ry) {
105
- rx = !rx ? ry : rx;
106
- ry = !ry ? rx : ry;
107
-
108
- /* istanbul ignore else */
109
- if (rx * 2 > w) rx -= (rx * 2 - w) / 2;
110
- /* istanbul ignore else */
111
- if (ry * 2 > h) ry -= (ry * 2 - h) / 2;
112
-
113
- return [
114
- ['M', x + rx, y],
115
- ['h', w - rx * 2],
116
- ['s', rx, 0, rx, ry],
117
- ['v', h - ry * 2],
118
- ['s', 0, ry, -rx, ry],
119
- ['h', -w + rx * 2],
120
- ['s', -rx, 0, -rx, -ry],
121
- ['v', -h + ry * 2],
122
- ['s', 0, -ry, rx, -ry],
123
- ];
124
- }
125
-
126
- return [['M', x, y], ['h', w], ['v', h], ['H', x], ['Z']];
127
- };
7
+ import shapeToPathArray from './shapeToPathArray';
8
+ import shapeParams from './shapeParams';
128
9
 
129
10
  /**
130
11
  * Returns a new `<path>` element created from attributes of a `<line>`, `<polyline>`,
@@ -133,7 +14,7 @@ export const getRectanglePath = (attr: RectAttr): PathArray => {
133
14
  * `document` browser page, if you want to use in server-side using `jsdom`, you can
134
15
  * pass the `jsdom` `document` to `ownDocument`.
135
16
  *
136
- * It can also work with an options object,
17
+ * It can also work with an options object, see the type below
137
18
  *
138
19
  * @see ShapeOps
139
20
  *
@@ -153,21 +34,25 @@ const shapeToPath = (
153
34
  const doc = ownerDocument || document;
154
35
  const win = doc.defaultView || /* istanbul ignore next */ window;
155
36
  const supportedShapes = Object.keys(shapeParams) as (keyof ShapeParams)[];
156
- const elementIsElement = element instanceof win.SVGElement;
157
- const tagName = elementIsElement ? element.tagName : null;
37
+ const targetIsElement = element instanceof win.SVGElement;
38
+ const tagName = targetIsElement ? element.tagName : null;
158
39
 
159
- if (tagName && supportedShapes.every(s => tagName !== s)) {
160
- throw TypeError(`${error}: "${tagName}" is not SVGElement`);
161
- }
40
+ if (tagName === 'path') throw TypeError(`${error}: "${tagName}" is already SVGPathElement`);
41
+ if (tagName && supportedShapes.every(s => tagName !== s)) throw TypeError(`${error}: "${tagName}" is not SVGElement`);
162
42
 
163
43
  const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
164
- const type = (elementIsElement ? tagName : element.type) as ShapeOps['type'];
44
+ const type = (targetIsElement ? tagName : element.type) as ShapeOps['type'];
165
45
  const shapeAttrs = shapeParams[type] as string[];
166
46
  const config = { type } as Record<string, string>;
167
47
 
168
- if (elementIsElement) {
48
+ // set d
49
+ const round = defaultOptions.round as number;
50
+ const pathArray = shapeToPathArray(element, doc);
51
+ const description = pathArray && pathArray.length ? pathToString(pathArray, round) : '';
52
+
53
+ if (targetIsElement) {
169
54
  shapeAttrs.forEach(p => {
170
- if (shapeAttrs.includes(p)) config[p] = element.getAttribute(p) as string;
55
+ config[p] = element.getAttribute(p) as string;
171
56
  });
172
57
  // set no-specific shape attributes: fill, stroke, etc
173
58
  Object.values(element.attributes).forEach(({ name, value }) => {
@@ -186,24 +71,10 @@ const shapeToPath = (
186
71
  });
187
72
  }
188
73
 
189
- // set d
190
- let description = '';
191
- const round = defaultOptions.round as number;
192
-
193
- /* istanbul ignore else */
194
- if (type === 'circle') description = pathToString(getCirclePath(config as unknown as CircleAttr), round);
195
- else if (type === 'ellipse') description = pathToString(getEllipsePath(config as unknown as EllipseAttr), round);
196
- else if (['polyline', 'polygon'].includes(type))
197
- description = pathToString(getPolyPath(config as unknown as PolyAttr), round);
198
- else if (type === 'rect') description = pathToString(getRectanglePath(config as unknown as RectAttr), round);
199
- else if (type === 'line') description = pathToString(getLinePath(config as unknown as LineAttr), round);
200
- else if (type === 'glyph')
201
- description = elementIsElement ? (element.getAttribute('d') as string) : (element as GlyphAttr).d;
202
-
203
74
  // replace target element
204
75
  if (isValidPath(description)) {
205
76
  path.setAttribute('d', description);
206
- if (replace && elementIsElement) {
77
+ if (replace && targetIsElement) {
207
78
  element.before(path, element);
208
79
  element.remove();
209
80
  }
@@ -211,4 +82,5 @@ const shapeToPath = (
211
82
  }
212
83
  return false;
213
84
  };
85
+
214
86
  export default shapeToPath;
@@ -0,0 +1,179 @@
1
+ import type { CircleAttr, EllipseAttr, GlyphAttr, LineAttr, PolyAttr, RectAttr, ShapeParams } from '../interface';
2
+ import type { PathArray, PathSegment, ShapeOps, ShapeTypes } from '../types';
3
+ import error from '../parser/error';
4
+ import parsePathString from '../parser/parsePathString';
5
+ import shapeParams from './shapeParams';
6
+ import isPathArray from './isPathArray';
7
+
8
+ /**
9
+ * Returns a new `pathArray` from line attributes.
10
+ *
11
+ * @param attr shape configuration
12
+ * @returns a new line `pathArray`
13
+ */
14
+ export const getLinePath = (attr: LineAttr): PathArray => {
15
+ let { x1, y1, x2, y2 } = attr;
16
+ [x1, y1, x2, y2] = [x1, y1, x2, y2].map(a => +a);
17
+ return [
18
+ ['M', x1, y1],
19
+ ['L', x2, y2],
20
+ ];
21
+ };
22
+
23
+ /**
24
+ * Returns a new `pathArray` like from polyline/polygon attributes.
25
+ *
26
+ * @param attr shape configuration
27
+ * @return a new polygon/polyline `pathArray`
28
+ */
29
+ export const getPolyPath = (attr: PolyAttr): PathArray => {
30
+ const pathArray = [] as PathSegment[];
31
+ const points = (attr.points || '')
32
+ .trim()
33
+ .split(/[\s|,]/)
34
+ .map(a => +a);
35
+
36
+ let index = 0;
37
+ while (index < points.length) {
38
+ pathArray.push([index ? 'L' : 'M', points[index], points[index + 1]]);
39
+ index += 2;
40
+ }
41
+
42
+ return (attr.type === 'polygon' ? [...pathArray, ['z']] : pathArray) as PathArray;
43
+ };
44
+
45
+ /**
46
+ * Returns a new `pathArray` from circle attributes.
47
+ *
48
+ * @param attr shape configuration
49
+ * @return a circle `pathArray`
50
+ */
51
+ export const getCirclePath = (attr: CircleAttr): PathArray => {
52
+ let { cx, cy, r } = attr;
53
+ [cx, cy, r] = [cx, cy, r].map(a => +a);
54
+
55
+ return [
56
+ ['M', cx - r, cy],
57
+ ['a', r, r, 0, 1, 0, 2 * r, 0],
58
+ ['a', r, r, 0, 1, 0, -2 * r, 0],
59
+ ];
60
+ };
61
+
62
+ /**
63
+ * Returns a new `pathArray` from ellipse attributes.
64
+ *
65
+ * @param attr shape configuration
66
+ * @return an ellipse `pathArray`
67
+ */
68
+ export const getEllipsePath = (attr: EllipseAttr): PathArray => {
69
+ let { cx, cy } = attr;
70
+ let rx = attr.rx || 0;
71
+ let ry = attr.ry || rx;
72
+ [cx, cy, rx, ry] = [cx, cy, rx, ry].map(a => +a);
73
+
74
+ return [
75
+ ['M', cx - rx, cy],
76
+ ['a', rx, ry, 0, 1, 0, 2 * rx, 0],
77
+ ['a', rx, ry, 0, 1, 0, -2 * rx, 0],
78
+ ];
79
+ };
80
+
81
+ /**
82
+ * Returns a new `pathArray` like from rect attributes.
83
+ *
84
+ * @param attr object with properties above
85
+ * @return a new `pathArray` from `<rect>` attributes
86
+ */
87
+ export const getRectanglePath = (attr: RectAttr): PathArray => {
88
+ const x = +attr.x || 0;
89
+ const y = +attr.y || 0;
90
+ const w = +attr.width;
91
+ const h = +attr.height;
92
+ let rx = +(attr.rx || 0);
93
+ let ry = +(attr.ry || rx);
94
+
95
+ // Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement:
96
+ if (rx || ry) {
97
+ // rx = !rx ? ry : rx;
98
+ // ry = !ry ? rx : ry;
99
+
100
+ /* istanbul ignore else */
101
+ if (rx * 2 > w) rx -= (rx * 2 - w) / 2;
102
+ /* istanbul ignore else */
103
+ if (ry * 2 > h) ry -= (ry * 2 - h) / 2;
104
+
105
+ return [
106
+ ['M', x + rx, y],
107
+ ['h', w - rx * 2],
108
+ ['s', rx, 0, rx, ry],
109
+ ['v', h - ry * 2],
110
+ ['s', 0, ry, -rx, ry],
111
+ ['h', -w + rx * 2],
112
+ ['s', -rx, 0, -rx, -ry],
113
+ ['v', -h + ry * 2],
114
+ ['s', 0, -ry, rx, -ry],
115
+ ];
116
+ }
117
+
118
+ return [['M', x, y], ['h', w], ['v', h], ['H', x], ['Z']];
119
+ };
120
+
121
+ /**
122
+ * Returns a new `pathArray` created from attributes of a `<line>`, `<polyline>`,
123
+ * `<polygon>`, `<rect>`, `<ellipse>`, `<circle>`, <path> or `<glyph>`.
124
+ *
125
+ * The default `ownerDocument` is your current `document` browser page,
126
+ * if you want to use in server-side using `jsdom`, you can pass the
127
+ * `jsdom` `document` to `ownDocument`.
128
+ *
129
+ * It can also work with an options object, see the type below
130
+ *
131
+ * @see ShapeOps
132
+ *
133
+ * @param element target shape
134
+ * @param ownerDocument document for create element
135
+ * @return the newly created `<path>` element
136
+ */
137
+ const shapeToPathArray = (element: ShapeTypes | ShapeOps, ownerDocument?: Document): PathArray | false => {
138
+ const doc = ownerDocument || document;
139
+ const win = doc.defaultView || /* istanbul ignore next */ window;
140
+ const supportedShapes = Object.keys(shapeParams) as (keyof ShapeParams)[];
141
+ const targetIsElement = element instanceof win.SVGElement;
142
+ const tagName = targetIsElement ? element.tagName : null;
143
+
144
+ if (tagName && [...supportedShapes, 'path'].every(s => tagName !== s)) {
145
+ throw TypeError(`${error}: "${tagName}" is not SVGElement`);
146
+ }
147
+
148
+ const type = (targetIsElement ? tagName : element.type) as ShapeOps['type'];
149
+ const shapeAttrs = shapeParams[type] as string[];
150
+ const config = { type } as Record<string, string>;
151
+
152
+ if (targetIsElement) {
153
+ shapeAttrs.forEach(p => {
154
+ config[p] = element.getAttribute(p) as string;
155
+ });
156
+ } else {
157
+ Object.assign(config, element);
158
+ }
159
+
160
+ // set d
161
+ let pathArray = [] as unknown as PathArray;
162
+
163
+ /* istanbul ignore else */
164
+ if (type === 'circle') pathArray = getCirclePath(config as unknown as CircleAttr);
165
+ else if (type === 'ellipse') pathArray = getEllipsePath(config as unknown as EllipseAttr);
166
+ else if (['polyline', 'polygon'].includes(type)) pathArray = getPolyPath(config as unknown as PolyAttr);
167
+ else if (type === 'rect') pathArray = getRectanglePath(config as unknown as RectAttr);
168
+ else if (type === 'line') pathArray = getLinePath(config as unknown as LineAttr);
169
+ else if (['glyph', 'path'].includes(type)) {
170
+ pathArray = parsePathString(targetIsElement ? element.getAttribute('d') || '' : (element as GlyphAttr).d || '');
171
+ }
172
+
173
+ // replace target element
174
+ if (isPathArray(pathArray) && pathArray.length) {
175
+ return pathArray;
176
+ }
177
+ return false;
178
+ };
179
+ export default shapeToPathArray;