headless-svg-to-excalidraw 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Excalidraw
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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # svg-to-excalidraw
2
+
3
+ Library to convert SVG to Excalidraw’s file format.
4
+
5
+ ## :floppy_disk: Installation
6
+
7
+ ```bash
8
+ yarn add svg-to-excalidraw
9
+ ```
10
+
11
+ ## :beginner: Usage
12
+
13
+ ```typescript
14
+ import svgToEx from "svg-to-excalidraw";
15
+
16
+ const heartSVG = `
17
+ <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
18
+ <path d="M 10,30
19
+ A 20,20 0,0,1 50,30
20
+ A 20,20 0,0,1 90,30
21
+ Q 90,60 50,90
22
+ Q 10,60 10,30 z"/>
23
+ </svg>
24
+ `;
25
+
26
+ const { hasErrors, errors, content } = svgToEx.convert(heartSVG);
27
+
28
+ // SVG parsing errors are propagated through.
29
+ if (hasErrors) {
30
+ console.error(errors);
31
+ return;
32
+ }
33
+
34
+ navigator.clipboard.writeText(content);
35
+
36
+ // the heart excalidraw json is now copied to your clipboard.
37
+ // Just Paste it into your Excalidraw session!
38
+ ```
39
+
40
+ ## :game_die: Running tests
41
+
42
+ TODO.
43
+
44
+ ### :building_construction: Local Development
45
+
46
+ #### Building the Project
47
+
48
+ ```bash
49
+ yarn build
50
+
51
+ # Build and watch whenever a file is updated
52
+ yarn build:watch
53
+ ```
54
+
55
+ ## :busts_in_silhouette: Contributing
56
+
57
+ Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/svg-to-excalidraw/issues) first to discuss what you would like to change.
@@ -0,0 +1,10 @@
1
+ //#region src/parser.d.ts
2
+ type ConversionResult = {
3
+ hasErrors: boolean;
4
+ errors: any;
5
+ content: any;
6
+ warnings: string[];
7
+ };
8
+ declare const convert: (svgString: string) => ConversionResult;
9
+ //#endregion
10
+ export { convert };
package/dist/index.mjs ADDED
@@ -0,0 +1,656 @@
1
+ import { mat4, vec3 } from "gl-matrix";
2
+ import { Random } from "roughjs/bin/math";
3
+ import { nanoid } from "nanoid";
4
+ import chroma from "chroma-js";
5
+ import { DOMParser, parseHTML } from "linkedom";
6
+ import { pointsOnPath } from "points-on-path";
7
+
8
+ //#region src/elements/ExcalidrawScene.ts
9
+ var ExcalidrawScene = class {
10
+ type = "excalidraw";
11
+ version = 2;
12
+ source = "https://excalidraw.com";
13
+ elements = [];
14
+ constructor(elements = []) {
15
+ this.elements = elements;
16
+ }
17
+ toExJSON() {
18
+ return {
19
+ ...this,
20
+ elements: this.elements.map((el) => ({ ...el }))
21
+ };
22
+ }
23
+ };
24
+ var ExcalidrawScene_default = ExcalidrawScene;
25
+
26
+ //#endregion
27
+ //#region src/utils.ts
28
+ const random = new Random(Date.now());
29
+ const randomInteger = () => Math.floor(random.next() * 2 ** 31);
30
+ const randomId = () => nanoid();
31
+ function dimensionsFromPoints(points) {
32
+ const xCoords = points.map(([x]) => x);
33
+ const yCoords = points.map(([, y]) => y);
34
+ const minX = Math.min(...xCoords);
35
+ const minY = Math.min(...yCoords);
36
+ const maxX = Math.max(...xCoords);
37
+ const maxY = Math.max(...yCoords);
38
+ return [maxX - minX, maxY - minY];
39
+ }
40
+ function getWindingOrder(points) {
41
+ return points.reduce((acc, [x1, y1], idx, arr) => {
42
+ const p2 = arr[idx + 1];
43
+ const x2 = p2 ? p2[0] : 0;
44
+ const y2 = p2 ? p2[1] : 0;
45
+ return (x2 - x1) * (y2 + y1) + acc;
46
+ }, 0) > 0 ? "clockwise" : "counterclockwise";
47
+ }
48
+
49
+ //#endregion
50
+ //#region src/attributes.ts
51
+ function hexWithAlpha(color, alpha) {
52
+ return chroma(color).alpha(alpha).css();
53
+ }
54
+ function has(el, attr) {
55
+ return el.hasAttribute(attr);
56
+ }
57
+ function get(el, attr, backup) {
58
+ return el.getAttribute(attr) || backup || "";
59
+ }
60
+ function getNum(el, attr, backup) {
61
+ const numVal = Number(get(el, attr));
62
+ return Number.isNaN(numVal) ? backup || 0 : numVal;
63
+ }
64
+ const attrHandlers = {
65
+ stroke: ({ el, exVals }) => {
66
+ const strokeColor = get(el, "stroke");
67
+ exVals.strokeColor = has(el, "stroke-opacity") ? hexWithAlpha(strokeColor, getNum(el, "stroke-opacity")) : strokeColor;
68
+ },
69
+ "stroke-opacity": ({ el, exVals }) => {
70
+ exVals.strokeColor = hexWithAlpha(get(el, "stroke", "#000000"), getNum(el, "stroke-opacity"));
71
+ },
72
+ "stroke-width": ({ el, exVals }) => {
73
+ exVals.strokeWidth = getNum(el, "stroke-width");
74
+ },
75
+ fill: ({ el, exVals }) => {
76
+ const fill = get(el, `fill`);
77
+ exVals.backgroundColor = fill === "none" ? "#00000000" : fill;
78
+ },
79
+ "fill-opacity": ({ el, exVals }) => {
80
+ exVals.backgroundColor = hexWithAlpha(get(el, "fill", "#000000"), getNum(el, "fill-opacity"));
81
+ },
82
+ opacity: ({ el, exVals }) => {
83
+ exVals.opacity = getNum(el, "opacity", 100);
84
+ }
85
+ };
86
+ function presAttrsToElementValues(el) {
87
+ return [...el.attributes].reduce((exVals, attr) => {
88
+ const name = attr.name;
89
+ if (Object.keys(attrHandlers).includes(name)) attrHandlers[name]({
90
+ el,
91
+ exVals
92
+ });
93
+ return exVals;
94
+ }, {});
95
+ }
96
+ function filterAttrsToElementValues(el) {
97
+ const filterVals = {};
98
+ if (has(el, "x")) filterVals.x = getNum(el, "x");
99
+ if (has(el, "y")) filterVals.y = getNum(el, "y");
100
+ if (has(el, "width")) filterVals.width = getNum(el, "width");
101
+ if (has(el, "height")) filterVals.height = getNum(el, "height");
102
+ return filterVals;
103
+ }
104
+ function pointsAttrToPoints(el) {
105
+ let points = [];
106
+ if (has(el, "points")) points = get(el, "points").split(" ").map((p) => p.split(",").map(parseFloat));
107
+ return points;
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/elements/Group.ts
112
+ function getGroupAttrs(groups) {
113
+ return groups.reduce((acc, { element }) => {
114
+ const elVals = presAttrsToElementValues(element);
115
+ return {
116
+ ...acc,
117
+ ...elVals
118
+ };
119
+ }, {});
120
+ }
121
+ var Group = class {
122
+ id = randomId();
123
+ element;
124
+ constructor(element) {
125
+ this.element = element;
126
+ }
127
+ };
128
+ var Group_default = Group;
129
+
130
+ //#endregion
131
+ //#region src/dom.ts
132
+ /**
133
+ * DOM API provider using linkedom
134
+ * Works in browser, Node.js, and serverless/edge environments
135
+ */
136
+ const dom = parseHTML("<!DOCTYPE html>");
137
+ const DOMParser$1 = new DOMParser();
138
+ const document = dom.document;
139
+ const NodeFilter = {
140
+ SHOW_ALL: 4294967295,
141
+ SHOW_ELEMENT: 1,
142
+ SHOW_ATTRIBUTE: 2,
143
+ SHOW_TEXT: 4,
144
+ SHOW_CDATA_SECTION: 8,
145
+ SHOW_ENTITY_REFERENCE: 16,
146
+ SHOW_ENTITY: 32,
147
+ SHOW_PROCESSING_INSTRUCTION: 64,
148
+ SHOW_COMMENT: 128,
149
+ SHOW_DOCUMENT: 256,
150
+ SHOW_DOCUMENT_TYPE: 512,
151
+ SHOW_DOCUMENT_FRAGMENT: 1024,
152
+ SHOW_NOTATION: 2048,
153
+ FILTER_ACCEPT: 1,
154
+ FILTER_REJECT: 2,
155
+ FILTER_SKIP: 3
156
+ };
157
+
158
+ //#endregion
159
+ //#region src/elements/ExcalidrawElement.ts
160
+ function createExElement() {
161
+ return {
162
+ id: randomId(),
163
+ x: 0,
164
+ y: 0,
165
+ strokeColor: "#000000",
166
+ backgroundColor: "#000000",
167
+ fillStyle: "solid",
168
+ strokeWidth: 1,
169
+ strokeStyle: "solid",
170
+ strokeSharpness: "sharp",
171
+ roughness: 0,
172
+ opacity: 100,
173
+ width: 0,
174
+ height: 0,
175
+ angle: 0,
176
+ seed: randomInteger(),
177
+ version: 0,
178
+ versionNonce: 0,
179
+ isDeleted: false,
180
+ groupIds: [],
181
+ boundElementIds: null
182
+ };
183
+ }
184
+ function createExRect() {
185
+ return {
186
+ ...createExElement(),
187
+ type: "rectangle"
188
+ };
189
+ }
190
+ function createExLine() {
191
+ return {
192
+ ...createExElement(),
193
+ type: "line",
194
+ points: []
195
+ };
196
+ }
197
+ function createExEllipse() {
198
+ return {
199
+ ...createExElement(),
200
+ type: "ellipse"
201
+ };
202
+ }
203
+ function createExDraw() {
204
+ return {
205
+ ...createExElement(),
206
+ type: "draw",
207
+ points: []
208
+ };
209
+ }
210
+
211
+ //#endregion
212
+ //#region src/transform.ts
213
+ const transformFunctionsArr = Object.keys({
214
+ matrix: "matrix",
215
+ matrix3d: "matrix3d",
216
+ perspective: "perspective",
217
+ rotate: "rotate",
218
+ rotate3d: "rotate3d",
219
+ rotateX: "rotateX",
220
+ rotateY: "rotateY",
221
+ rotateZ: "rotateZ",
222
+ scale: "scale",
223
+ scale3d: "scale3d",
224
+ scaleX: "scaleX",
225
+ scaleY: "scaleY",
226
+ scaleZ: "scaleZ",
227
+ skew: "skew",
228
+ skewX: "skewX",
229
+ skewY: "skewY",
230
+ translate: "translate",
231
+ translate3d: "translate3d",
232
+ translateX: "translateX",
233
+ translateY: "translateY",
234
+ translateZ: "translateZ"
235
+ });
236
+ const defaultUnits = {
237
+ matrix: "",
238
+ matrix3d: "",
239
+ perspective: "perspective",
240
+ rotate: "deg",
241
+ rotate3d: "deg",
242
+ rotateX: "deg",
243
+ rotateY: "deg",
244
+ rotateZ: "deg",
245
+ scale: "",
246
+ scale3d: "",
247
+ scaleX: "",
248
+ scaleY: "",
249
+ scaleZ: "",
250
+ skew: "skew",
251
+ skewX: "deg",
252
+ skewY: "deg",
253
+ translate: "px",
254
+ translate3d: "px",
255
+ translateX: "px",
256
+ translateY: "px",
257
+ translateZ: "px"
258
+ };
259
+ const svgTransformToCSSTransform = (svgTransformStr) => {
260
+ const tFuncs = svgTransformStr.match(/(\w+)\(([^)]*)\)/g);
261
+ if (!tFuncs) return "";
262
+ return tFuncs.map((tFuncStr) => {
263
+ const type = tFuncStr.split("(")[0];
264
+ if (!type) throw new Error("Unable to find transform name");
265
+ if (!transformFunctionsArr.includes(type)) throw new Error(`transform function name "${type}" is not valid`);
266
+ const tFuncParts = tFuncStr.match(/([-+]?[0-9]*\.?[0-9]+)([a-z])*/g);
267
+ if (!tFuncParts) return {
268
+ type,
269
+ values: []
270
+ };
271
+ let values = tFuncParts.map((a) => {
272
+ const [value, unit] = a.matchAll(/([-+]?[0-9]*\.?[0-9]+)|([a-z])*/g);
273
+ return {
274
+ unit: unit[0] || defaultUnits[type],
275
+ value: value[0]
276
+ };
277
+ });
278
+ if (values && type === "rotate" && values?.length > 1) values = [values[0]];
279
+ return {
280
+ type,
281
+ values
282
+ };
283
+ }).map(({ type, values }) => {
284
+ return `${type}(${values.map(({ unit, value }) => `${value}${unit}`).join(", ")})`;
285
+ }).join(" ");
286
+ };
287
+ function getElementMatrix(el) {
288
+ if (el.hasAttribute("transform")) {
289
+ const elMat = new DOMMatrix(svgTransformToCSSTransform(el.getAttribute("transform") || ""));
290
+ return mat4.multiply(mat4.create(), mat4.create(), elMat.toFloat32Array());
291
+ }
292
+ return mat4.create();
293
+ }
294
+ function getTransformMatrix(el, groups) {
295
+ return groups.map(({ element }) => getElementMatrix(element)).concat([getElementMatrix(el)]).reduce((acc, mat) => mat4.multiply(acc, acc, mat), mat4.create());
296
+ }
297
+ function transformPoints(points, transform) {
298
+ return points.map(([x, y]) => {
299
+ const [newX, newY] = vec3.transformMat4(vec3.create(), vec3.fromValues(x, y, 1), transform);
300
+ return [newX, newY];
301
+ });
302
+ }
303
+
304
+ //#endregion
305
+ //#region src/walker.ts
306
+ const SUPPORTED_TAGS = [
307
+ "svg",
308
+ "path",
309
+ "g",
310
+ "use",
311
+ "circle",
312
+ "ellipse",
313
+ "rect",
314
+ "polyline",
315
+ "polygon"
316
+ ];
317
+ const nodeValidator = (node) => {
318
+ if (node.nodeType !== 1) return NodeFilter.FILTER_REJECT;
319
+ const element = node;
320
+ if (SUPPORTED_TAGS.includes(element.tagName.toLowerCase())) return NodeFilter.FILTER_ACCEPT;
321
+ return NodeFilter.FILTER_REJECT;
322
+ };
323
+ function createTreeWalker(dom) {
324
+ return (dom.ownerDocument || dom).createTreeWalker(dom, NodeFilter.SHOW_ELEMENT, { acceptNode: nodeValidator });
325
+ }
326
+ /**
327
+ * Helper to simulate TreeWalker.nextSibling() for linkedom compatibility
328
+ * Linkedom's TreeWalker only implements nextNode(), not nextSibling()
329
+ */
330
+ function getNextSibling(tw, currentNode) {
331
+ const startNode = currentNode;
332
+ let node = tw.nextNode();
333
+ while (node) {
334
+ let parent = node.parentNode;
335
+ let isDescendant = false;
336
+ while (parent) {
337
+ if (parent === startNode) {
338
+ isDescendant = true;
339
+ break;
340
+ }
341
+ parent = parent.parentNode;
342
+ }
343
+ if (!isDescendant) return node;
344
+ node = tw.nextNode();
345
+ }
346
+ return null;
347
+ }
348
+ const presAttrs = (el, groups) => {
349
+ return {
350
+ ...getGroupAttrs(groups),
351
+ ...presAttrsToElementValues(el),
352
+ ...filterAttrsToElementValues(el)
353
+ };
354
+ };
355
+ const skippedUseAttrs = ["id"];
356
+ const allwaysPassedUseAttrs = [
357
+ "x",
358
+ "y",
359
+ "width",
360
+ "height",
361
+ "href",
362
+ "xlink:href"
363
+ ];
364
+ const getDefElWithCorrectAttrs = (defEl, useEl) => {
365
+ return [...useEl.attributes].reduce((el, attr) => {
366
+ if (skippedUseAttrs.includes(attr.value)) return el;
367
+ if (!defEl.hasAttribute(attr.name) || allwaysPassedUseAttrs.includes(attr.name)) el.setAttribute(attr.name, useEl.getAttribute(attr.name) || "");
368
+ return el;
369
+ }, defEl.cloneNode());
370
+ };
371
+ const walkers = {
372
+ svg: (args) => {
373
+ walk(args, args.tw.nextNode());
374
+ },
375
+ g: (args) => {
376
+ const groupNode = args.tw.currentNode;
377
+ const nextArgs = {
378
+ ...args,
379
+ tw: createTreeWalker(groupNode),
380
+ groups: [...args.groups, new Group_default(groupNode)]
381
+ };
382
+ walk(nextArgs, nextArgs.tw.nextNode());
383
+ walk(args, getNextSibling(args.tw, groupNode));
384
+ },
385
+ use: (args) => {
386
+ const { root, tw, scene } = args;
387
+ const useEl = tw.currentNode;
388
+ const id = useEl.getAttribute("href") || useEl.getAttribute("xlink:href");
389
+ if (!id) throw new Error("unable to get id of use element");
390
+ const defEl = root.querySelector(id);
391
+ if (!defEl) throw new Error(`unable to find def element with id: ${id}`);
392
+ const tempScene = new ExcalidrawScene_default();
393
+ const finalEl = getDefElWithCorrectAttrs(defEl, useEl);
394
+ walk({
395
+ ...args,
396
+ scene: tempScene,
397
+ tw: createTreeWalker(finalEl)
398
+ }, finalEl);
399
+ const exEl = tempScene.elements.pop();
400
+ if (!exEl) throw new Error("Unable to create ex element");
401
+ scene.elements.push(exEl);
402
+ walk(args, args.tw.nextNode());
403
+ },
404
+ circle: (args) => {
405
+ const { tw, scene, groups } = args;
406
+ const el = tw.currentNode;
407
+ const r = getNum(el, "r", 0);
408
+ const d = r * 2;
409
+ const x = getNum(el, "x", 0) + getNum(el, "cx", 0) - r;
410
+ const y = getNum(el, "y", 0) + getNum(el, "cy", 0) - r;
411
+ const mat = getTransformMatrix(el, groups);
412
+ const m = mat4.fromValues(d, 0, 0, 0, 0, d, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
413
+ const result = mat4.multiply(mat4.create(), mat, m);
414
+ const circle = {
415
+ ...createExEllipse(),
416
+ ...presAttrs(el, groups),
417
+ x: result[12],
418
+ y: result[13],
419
+ width: result[0],
420
+ height: result[5],
421
+ groupIds: groups.map((g) => g.id)
422
+ };
423
+ scene.elements.push(circle);
424
+ walk(args, tw.nextNode());
425
+ },
426
+ ellipse: (args) => {
427
+ const { tw, scene, groups } = args;
428
+ const el = tw.currentNode;
429
+ const rx = getNum(el, "rx", 0);
430
+ const ry = getNum(el, "ry", 0);
431
+ const cx = getNum(el, "cx", 0);
432
+ const cy = getNum(el, "cy", 0);
433
+ const x = getNum(el, "x", 0) + cx - rx;
434
+ const y = getNum(el, "y", 0) + cy - ry;
435
+ const w = rx * 2;
436
+ const h = ry * 2;
437
+ const mat = getTransformMatrix(el, groups);
438
+ const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
439
+ const result = mat4.multiply(mat4.create(), mat, m);
440
+ const ellipse = {
441
+ ...createExEllipse(),
442
+ ...presAttrs(el, groups),
443
+ x: result[12],
444
+ y: result[13],
445
+ width: result[0],
446
+ height: result[5],
447
+ groupIds: groups.map((g) => g.id)
448
+ };
449
+ scene.elements.push(ellipse);
450
+ walk(args, tw.nextNode());
451
+ },
452
+ line: (args) => {
453
+ walk(args, args.tw.nextNode());
454
+ },
455
+ polygon: (args) => {
456
+ const { tw, scene, groups } = args;
457
+ const el = tw.currentNode;
458
+ const transformedPoints = transformPoints(pointsAttrToPoints(el), getTransformMatrix(el, groups));
459
+ const x = transformedPoints[0][0];
460
+ const y = transformedPoints[0][1];
461
+ const relativePoints = transformedPoints.map(([_x, _y]) => [_x - x, _y - y]);
462
+ const [width, height] = dimensionsFromPoints(relativePoints);
463
+ const line = {
464
+ ...createExLine(),
465
+ ...getGroupAttrs(groups),
466
+ ...presAttrsToElementValues(el),
467
+ points: relativePoints.concat([[0, 0]]),
468
+ x,
469
+ y,
470
+ width,
471
+ height
472
+ };
473
+ scene.elements.push(line);
474
+ walk(args, args.tw.nextNode());
475
+ },
476
+ polyline: (args) => {
477
+ const { tw, scene, groups } = args;
478
+ const el = tw.currentNode;
479
+ const mat = getTransformMatrix(el, groups);
480
+ const transformedPoints = transformPoints(pointsAttrToPoints(el), mat);
481
+ const x = transformedPoints[0][0];
482
+ const y = transformedPoints[0][1];
483
+ const relativePoints = transformedPoints.map(([_x, _y]) => [_x - x, _y - y]);
484
+ const [width, height] = dimensionsFromPoints(relativePoints);
485
+ const hasFill = has(el, "fill");
486
+ const fill = get(el, "fill");
487
+ const shouldFill = !hasFill || hasFill && fill !== "none";
488
+ const line = {
489
+ ...createExLine(),
490
+ ...getGroupAttrs(groups),
491
+ ...presAttrsToElementValues(el),
492
+ points: relativePoints.concat(shouldFill ? [[0, 0]] : []),
493
+ x,
494
+ y,
495
+ width,
496
+ height
497
+ };
498
+ scene.elements.push(line);
499
+ walk(args, args.tw.nextNode());
500
+ },
501
+ rect: (args) => {
502
+ const { tw, scene, groups } = args;
503
+ const el = tw.currentNode;
504
+ const x = getNum(el, "x", 0);
505
+ const y = getNum(el, "y", 0);
506
+ const w = getNum(el, "width", 0);
507
+ const h = getNum(el, "height", 0);
508
+ const mat = getTransformMatrix(el, groups);
509
+ const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
510
+ const result = mat4.multiply(mat4.create(), mat, m);
511
+ const isRound = el.hasAttribute("rx") || el.hasAttribute("ry");
512
+ const rect = {
513
+ ...createExRect(),
514
+ ...presAttrs(el, groups),
515
+ x: result[12],
516
+ y: result[13],
517
+ width: result[0],
518
+ height: result[5],
519
+ strokeSharpness: isRound ? "round" : "sharp"
520
+ };
521
+ scene.elements.push(rect);
522
+ walk(args, args.tw.nextNode());
523
+ },
524
+ path: (args) => {
525
+ const { tw, scene, groups } = args;
526
+ const el = tw.currentNode;
527
+ const mat = getTransformMatrix(el, groups);
528
+ const points = pointsOnPath(get(el, "d"));
529
+ const fillColor = get(el, "fill", "black");
530
+ const fillRule = get(el, "fill-rule", "nonzero");
531
+ let elements = [];
532
+ let localGroup = randomId();
533
+ switch (fillRule) {
534
+ case "nonzero":
535
+ let initialWindingOrder = "clockwise";
536
+ elements = points.map((pointArr, idx) => {
537
+ const tPoints = transformPoints(pointArr, mat4.clone(mat));
538
+ const x = tPoints[0][0];
539
+ const y = tPoints[0][1];
540
+ const [width, height] = dimensionsFromPoints(tPoints);
541
+ const relativePoints = tPoints.map(([_x, _y]) => [_x - x, _y - y]);
542
+ const windingOrder = getWindingOrder(relativePoints);
543
+ if (idx === 0) {
544
+ initialWindingOrder = windingOrder;
545
+ localGroup = randomId();
546
+ }
547
+ let backgroundColor = fillColor;
548
+ if (initialWindingOrder !== windingOrder) backgroundColor = "#FFFFFF";
549
+ return {
550
+ ...createExDraw(),
551
+ strokeWidth: 0,
552
+ strokeColor: "#00000000",
553
+ ...presAttrs(el, groups),
554
+ points: relativePoints,
555
+ backgroundColor,
556
+ width,
557
+ height,
558
+ x: x + getNum(el, "x", 0),
559
+ y: y + getNum(el, "y", 0),
560
+ groupIds: [localGroup]
561
+ };
562
+ });
563
+ break;
564
+ case "evenodd":
565
+ elements = points.map((pointArr, idx) => {
566
+ const tPoints = transformPoints(pointArr, mat4.clone(mat));
567
+ const x = tPoints[0][0];
568
+ const y = tPoints[0][1];
569
+ const [width, height] = dimensionsFromPoints(tPoints);
570
+ const relativePoints = tPoints.map(([_x, _y]) => [_x - x, _y - y]);
571
+ if (idx === 0) localGroup = randomId();
572
+ return {
573
+ ...createExDraw(),
574
+ ...presAttrs(el, groups),
575
+ points: relativePoints,
576
+ width,
577
+ height,
578
+ x: x + getNum(el, "x", 0),
579
+ y: y + getNum(el, "y", 0)
580
+ };
581
+ });
582
+ break;
583
+ default:
584
+ }
585
+ scene.elements = scene.elements.concat(elements);
586
+ walk(args, tw.nextNode());
587
+ }
588
+ };
589
+ function walk(args, nextNode) {
590
+ if (!nextNode) return;
591
+ const nodeName = nextNode.nodeName.toLowerCase();
592
+ if (walkers[nodeName]) walkers[nodeName](args);
593
+ else {
594
+ if (args.skippedElements && nextNode.nodeType === 1) {
595
+ const element = nextNode;
596
+ args.skippedElements.add(element.tagName.toLowerCase());
597
+ }
598
+ walk(args, args.tw.nextNode());
599
+ }
600
+ }
601
+
602
+ //#endregion
603
+ //#region src/parser.ts
604
+ const convert = (svgString) => {
605
+ const warnings = [];
606
+ if (!svgString || svgString.trim().length === 0) {
607
+ const error = "SVG string is empty or invalid";
608
+ console.error(error);
609
+ return {
610
+ hasErrors: true,
611
+ errors: [error],
612
+ content: null,
613
+ warnings
614
+ };
615
+ }
616
+ const svgDOM = DOMParser$1.parseFromString(svgString, "image/svg+xml");
617
+ const errorsElements = svgDOM.querySelectorAll("parsererror");
618
+ const hasErrors = errorsElements.length > 0;
619
+ let content = null;
620
+ if (hasErrors) console.error("There were errors while parsing the given SVG: ", [...errorsElements].map((el) => el.innerHTML));
621
+ else {
622
+ const tw = createTreeWalker(svgDOM);
623
+ const scene = new ExcalidrawScene_default();
624
+ const groups = [];
625
+ const skippedElements = /* @__PURE__ */ new Set();
626
+ walk({
627
+ tw,
628
+ scene,
629
+ groups,
630
+ root: svgDOM,
631
+ skippedElements
632
+ }, tw.nextNode());
633
+ content = scene.toExJSON();
634
+ const elementCount = content?.elements?.length || 0;
635
+ if (elementCount === 0) {
636
+ const warning = "Conversion produced 0 elements - the SVG may contain only unsupported elements or be empty";
637
+ console.warn(warning);
638
+ warnings.push(warning);
639
+ }
640
+ if (skippedElements.size > 0) {
641
+ const warning = `Skipped unsupported elements: ${Array.from(skippedElements).join(", ")}`;
642
+ console.warn(warning);
643
+ warnings.push(warning);
644
+ }
645
+ if (elementCount > 0) console.log(`✓ Successfully converted ${elementCount} SVG elements to Excalidraw format`);
646
+ }
647
+ return {
648
+ hasErrors,
649
+ errors: hasErrors ? errorsElements : null,
650
+ content,
651
+ warnings
652
+ };
653
+ };
654
+
655
+ //#endregion
656
+ export { convert };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "headless-svg-to-excalidraw",
3
+ "version": "0.0.1",
4
+ "description": "Convert SVG to Excalidraw's file format, no browser required.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "package.json"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsdown",
23
+ "clean": "rm -rf dist",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "license": "MIT",
27
+ "devDependencies": {
28
+ "@types/chroma-js": "^2.1.3",
29
+ "tsdown": "^0.20.1",
30
+ "typescript": "^5.9.3"
31
+ },
32
+ "peerDependencies": {
33
+ "typescript": "^4.5.5"
34
+ },
35
+ "dependencies": {
36
+ "chroma-js": "^2.1.2",
37
+ "gl-matrix": "^3.3.0",
38
+ "linkedom": "^0.18.12",
39
+ "nanoid": "5.0.9",
40
+ "path-data-parser": "^0.1.0",
41
+ "points-on-curve": "^0.2.0",
42
+ "points-on-path": "^0.2.1",
43
+ "roughjs": "^4.4.1"
44
+ }
45
+ }