@weasel-js/svg 0.7.2 → 1.0.0

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Path, FillStyle, StyledRun, TextStyle, IngestCtx } from '@weasel-js/core';
1
+ import { Path, FillStyle, StyledRun, TextStyle, TilePatternSpec, IngestCtx } from '@weasel-js/core';
2
2
 
3
3
  /**
4
4
  * Public types for `@weasel-js/svg`. The package exposes a flat,
@@ -206,6 +206,12 @@ interface SerializeOptions {
206
206
  width: number;
207
207
  height: number;
208
208
  };
209
+ /**
210
+ * Called for paint that can't be expressed in SVG and is dropped — a
211
+ * conic gradient, or a pattern carrying a `TextureHandle` instead of a
212
+ * `TilePatternSpec`. Without this the loss is silent.
213
+ */
214
+ onWarn?: (message: string) => void;
209
215
  /** Emit `width="..."` on the root `<svg>`. */
210
216
  width?: number;
211
217
  /** Emit `height="..."` on the root `<svg>`. */
@@ -263,6 +269,33 @@ declare function parseSvg(svg: string, opts?: ParseOptions): ParseResult;
263
269
  */
264
270
  declare function serializeSvg(nodes: SvgNode[], opts?: SerializeOptions): string;
265
271
 
272
+ /**
273
+ * Map between SVG `<pattern>` elements and weasel's `pattern` `FillStyle`.
274
+ * The counterpart to `gradients.ts` for the other paint-server kind.
275
+ *
276
+ * The tile's shapes come from `tileGeometry` in core, so the vector form a
277
+ * viewer sees is generated from the same description that rasterizes the GL
278
+ * texture. Alongside them we write the spec itself onto a `data-weasel-tile`
279
+ * attribute: any SVG consumer renders the shapes, and weasel reads the
280
+ * attribute back to recover the exact spec rather than reverse-engineering
281
+ * it from geometry.
282
+ *
283
+ * A pattern whose payload is a `TextureHandle` has no vector form — the
284
+ * handle is a session-scoped registry key with no tile description behind
285
+ * it — and exports as nothing, with a warning. Same treatment conic
286
+ * gradients get.
287
+ */
288
+
289
+ /**
290
+ * A single tile as a standalone `<svg>` document, sized to its own extent.
291
+ * For UI that needs to show a tile outside a document — a picker swatch,
292
+ * set as a repeating CSS `background-image` via a data URI. Shares the shape
293
+ * mapper with `patternXml`, so a preview can't drift from what paints.
294
+ */
295
+ declare function tilePreviewSvg(spec: TilePatternSpec, background?: string): string;
296
+ /** `tilePreviewSvg` packed as a `url(...)` value for CSS `background-image`. */
297
+ declare function tilePreviewCssUrl(spec: TilePatternSpec, background?: string): string;
298
+
266
299
  interface SvgDraftBounds {
267
300
  x: number;
268
301
  y: number;
@@ -304,4 +337,4 @@ declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => st
304
337
  */
305
338
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
306
339
 
307
- export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, svgNodesToKitDrafts, unpackSvgFiles };
340
+ export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, PATH_C, PATH_Q } from '@weasel-js/core';
2
+ import { tileGeometry } from '@weasel-js/core/patterns-builtin';
2
3
 
3
4
  // src/parse.ts
4
5
 
@@ -241,6 +242,98 @@ function parsePaintAttr(raw) {
241
242
  return null;
242
243
  }
243
244
  }
245
+ function patternSpecOf(paint) {
246
+ return "tile" in paint.pattern ? paint.pattern : null;
247
+ }
248
+ function patternXml(id, paint, onWarn) {
249
+ const spec = patternSpecOf(paint);
250
+ if (!spec) {
251
+ onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
252
+ return "";
253
+ }
254
+ const geometry = tileGeometry(spec);
255
+ const body = geometry.shapes.map(shapeXml).join("");
256
+ const origin = paint.origin ?? { x: 0, y: 0 };
257
+ const attrs = [
258
+ `id="${id}"`,
259
+ 'patternUnits="userSpaceOnUse"',
260
+ `width="${trimNumber(geometry.size)}"`,
261
+ `height="${trimNumber(geometry.size)}"`
262
+ ];
263
+ if (origin.x !== 0 || origin.y !== 0) {
264
+ attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
265
+ }
266
+ attrs.push(`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`);
267
+ return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
268
+ }
269
+ function tilePreviewSvg(spec, background) {
270
+ const geometry = tileGeometry(spec);
271
+ const s = trimNumber(geometry.size);
272
+ const bg = background ? `<rect width="${s}" height="${s}" fill="${background}"/>` : "";
273
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}">${bg}${geometry.shapes.map(shapeXml).join("")}</svg>`;
274
+ }
275
+ function tilePreviewCssUrl(spec, background) {
276
+ return `url("data:image/svg+xml,${encodeURIComponent(tilePreviewSvg(spec, background))}")`;
277
+ }
278
+ function shapeXml(shape) {
279
+ switch (shape.kind) {
280
+ case "line":
281
+ return `<line x1="${trimNumber(shape.x1)}" y1="${trimNumber(shape.y1)}" x2="${trimNumber(shape.x2)}" y2="${trimNumber(shape.y2)}" stroke="${shape.color}" stroke-width="${trimNumber(shape.width)}"/>`;
282
+ case "circle":
283
+ return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
284
+ case "ellipse": {
285
+ const deg = shape.rotation * 180 / Math.PI;
286
+ const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
287
+ return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${shape.color}"${transform}/>`;
288
+ }
289
+ case "rect":
290
+ return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
291
+ default:
292
+ return "";
293
+ }
294
+ }
295
+ function collectPatterns(svg, onWarn) {
296
+ const out = /* @__PURE__ */ new Map();
297
+ const defs = svg.getElementsByTagName("defs");
298
+ for (let d = 0; d < defs.length; d++) {
299
+ const root = defs[d];
300
+ for (let i = 0; i < root.children.length; i++) {
301
+ const child = root.children[i];
302
+ if (child.tagName.toLowerCase() !== "pattern") continue;
303
+ const id = child.getAttribute("id");
304
+ if (!id) continue;
305
+ const paint = readPattern(child, onWarn);
306
+ if (paint) out.set(id, paint);
307
+ }
308
+ }
309
+ return out;
310
+ }
311
+ function readPattern(el, onWarn) {
312
+ const raw = el.getAttribute("data-weasel-tile");
313
+ if (!raw) {
314
+ onWarn?.(`<pattern id="${el.getAttribute("id")}"> has no data-weasel-tile \u2014 unsupported pattern, dropped`);
315
+ return null;
316
+ }
317
+ let spec;
318
+ try {
319
+ spec = JSON.parse(raw);
320
+ } catch {
321
+ onWarn?.(`<pattern id="${el.getAttribute("id")}"> has malformed data-weasel-tile \u2014 dropped`);
322
+ return null;
323
+ }
324
+ if (typeof spec?.tile !== "string" || typeof spec?.color !== "string") {
325
+ onWarn?.(`<pattern id="${el.getAttribute("id")}"> data-weasel-tile is not a tile spec \u2014 dropped`);
326
+ return null;
327
+ }
328
+ const x = parseFloat(el.getAttribute("x") ?? "0");
329
+ const y = parseFloat(el.getAttribute("y") ?? "0");
330
+ const paint = { fill: "pattern", pattern: spec };
331
+ if (x !== 0 || y !== 0) paint.origin = { x, y };
332
+ return paint;
333
+ }
334
+ function escapeAttr(s) {
335
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
336
+ }
244
337
 
245
338
  // src/gradients.ts
246
339
  function collectGradients(svg, onWarn) {
@@ -259,7 +352,7 @@ function collectGradients(svg, onWarn) {
259
352
  } else if (tag === "radialgradient") {
260
353
  const paint = readRadialGradient(child, onWarn);
261
354
  if (paint) out.set(id, paint);
262
- } else if (tag !== "lineargradient" && tag !== "radialgradient") {
355
+ } else if (tag !== "pattern") {
263
356
  onWarn?.(`unsupported <defs> child: <${child.tagName}>`);
264
357
  }
265
358
  }
@@ -315,25 +408,27 @@ function readRadialGradient(el, onWarn) {
315
408
  stops
316
409
  };
317
410
  }
318
- var GradientRegistry = class {
411
+ var PaintServerRegistry = class {
319
412
  byPaint = /* @__PURE__ */ new Map();
320
413
  order = [];
321
414
  counter = 0;
322
415
  register(paint) {
323
416
  const existing = this.byPaint.get(paint);
324
417
  if (existing) return existing;
325
- const id = `grad${this.counter++}`;
418
+ const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
326
419
  this.byPaint.set(paint, id);
327
420
  this.order.push(paint);
328
421
  return id;
329
422
  }
330
- /** Emit `<defs>...</defs>` XML for all registered gradients. */
331
- toDefsXml() {
423
+ /** Emit `<defs>...</defs>` XML for every registered paint server. */
424
+ toDefsXml(onWarn) {
332
425
  if (this.order.length === 0) return "";
333
426
  const parts = ["<defs>"];
334
427
  for (const paint of this.order) {
335
428
  const id = this.byPaint.get(paint);
336
- parts.push(gradientXml(id, paint));
429
+ parts.push(
430
+ paint.fill === "pattern" ? patternXml(id, paint, onWarn) : gradientXml(id, paint)
431
+ );
337
432
  }
338
433
  parts.push("</defs>");
339
434
  return parts.join("");
@@ -446,6 +541,7 @@ function parseSvg(svg, opts = {}) {
446
541
  }
447
542
  const documentMeta = collectDocumentMeta(root, uriToPrefix);
448
543
  const gradients = collectGradients(root, onWarn);
544
+ for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
449
545
  const rootStyle = deriveStyle(EMPTY_STYLE, root);
450
546
  const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, onWarn, uriToPrefix);
451
547
  const result = { nodes, warnings };
@@ -766,6 +862,40 @@ function readStroke(style, gradients, onWarn) {
766
862
  }
767
863
  return stroke;
768
864
  }
865
+ var STROKE_KEYS = [
866
+ "stroke",
867
+ "stroke-width",
868
+ "stroke-opacity",
869
+ "stroke-linecap",
870
+ "stroke-linejoin",
871
+ "stroke-dasharray",
872
+ "stroke-miterlimit"
873
+ ];
874
+ function ownStrokeStyle(el) {
875
+ const out = {};
876
+ for (const k of STROKE_KEYS) {
877
+ const v = ownProp(el, k);
878
+ if (v != null) out[k] = v;
879
+ }
880
+ return out;
881
+ }
882
+ function coreStroke(stroke) {
883
+ if (!stroke) return void 0;
884
+ let paint;
885
+ if (stroke.paint.kind === "gradient") {
886
+ paint = stroke.paint.paint;
887
+ } else if (stroke.paint.kind === "solid") {
888
+ paint = stroke.paint.opacity != null ? { fill: "solid", color: stroke.paint.color, opacity: stroke.paint.opacity } : { fill: "solid", color: stroke.paint.color };
889
+ } else {
890
+ return void 0;
891
+ }
892
+ const out = { paint, width: stroke.width };
893
+ if (stroke.cap) out.cap = stroke.cap;
894
+ if (stroke.join) out.join = stroke.join;
895
+ if (stroke.dash) out.dash = stroke.dash;
896
+ if (stroke.miterLimit != null) out.miterLimit = stroke.miterLimit;
897
+ return out;
898
+ }
769
899
  function parseDashArray(s) {
770
900
  const tokens = s.trim().split(/[\s,]+/).filter(Boolean);
771
901
  if (tokens.length === 0) return null;
@@ -816,7 +946,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
816
946
  const ax = m[0] * rawX + m[2] * rawY + m[4];
817
947
  const ay = m[1] * rawX + m[3] * rawY + m[5];
818
948
  const leafStyle = deriveStyle(style, el);
819
- const textStyle = readTextStyle(leafStyle, el, gradients, onWarn);
949
+ const textStyle = readTextStyle(leafStyle, gradients, onWarn);
820
950
  const fontSize = textStyle.fontSize ?? 16;
821
951
  const lineHeight = textStyle.lineHeight ?? 1.2;
822
952
  const dominantBaseline = el.getAttribute("dominant-baseline");
@@ -863,7 +993,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
863
993
  text: plain
864
994
  };
865
995
  const hasStyling = runs.some(
866
- (r) => r.bold || r.italic || r.fontFamily || r.fontSize != null || r.letterSpacing != null || r.underline || r.strikethrough || r.fill && ("color" in r.fill || "fill" in r.fill)
996
+ (r) => Object.entries(r).some(([k, v]) => k !== "text" && v !== void 0)
867
997
  );
868
998
  if (hasStyling) node.runs = runs;
869
999
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
@@ -916,9 +1046,11 @@ function readTspanRun(el, gradients, style, onWarn) {
916
1046
  if (paint) run.fill = paint;
917
1047
  }
918
1048
  }
1049
+ const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn));
1050
+ if (stroke) run.stroke = stroke;
919
1051
  return run;
920
1052
  }
921
- function readTextStyle(style, el, gradients, onWarn) {
1053
+ function readTextStyle(style, gradients, onWarn) {
922
1054
  const out = {};
923
1055
  const sz = style["font-size"];
924
1056
  if (sz != null) {
@@ -956,9 +1088,8 @@ function readTextStyle(style, el, gradients, onWarn) {
956
1088
  if (paint) out.fill = paint;
957
1089
  }
958
1090
  }
959
- if (el.hasAttribute("stroke")) {
960
- onWarn('<text stroke="..."> not supported on text; ignoring');
961
- }
1091
+ const stroke = coreStroke(readStroke(style, gradients, onWarn));
1092
+ if (stroke) out.stroke = stroke;
962
1093
  return out;
963
1094
  }
964
1095
  function serializePathD(path) {
@@ -997,14 +1128,14 @@ function serializePathD(path) {
997
1128
 
998
1129
  // src/serialize.ts
999
1130
  function serializeSvg(nodes, opts = {}) {
1000
- const registry = new GradientRegistry();
1001
- registerGradients(nodes, registry);
1131
+ const registry = new PaintServerRegistry();
1132
+ registerPaintServers(nodes, registry);
1002
1133
  const bounds = opts.viewBox ?? computeBounds(nodes);
1003
1134
  const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
1004
1135
  const namespaces = opts.namespaces ?? {};
1005
1136
  const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
1006
1137
  for (const [prefix, uri] of Object.entries(namespaces)) {
1007
- rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
1138
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
1008
1139
  }
1009
1140
  rootAttrs.push(`viewBox="${vb}"`);
1010
1141
  if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
@@ -1014,7 +1145,7 @@ function serializeSvg(nodes, opts = {}) {
1014
1145
  const bucket = opts.documentMeta[prefix];
1015
1146
  if (!bucket?.attrs) continue;
1016
1147
  for (const [name, value] of Object.entries(bucket.attrs)) {
1017
- rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
1148
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
1018
1149
  }
1019
1150
  }
1020
1151
  }
@@ -1028,7 +1159,7 @@ function serializeSvg(nodes, opts = {}) {
1028
1159
  }
1029
1160
  }
1030
1161
  }
1031
- const defsXml = registry.toDefsXml();
1162
+ const defsXml = registry.toDefsXml(opts.onWarn);
1032
1163
  const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces)).join("");
1033
1164
  const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
1034
1165
  return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
@@ -1036,7 +1167,7 @@ function serializeSvg(nodes, opts = {}) {
1036
1167
  function namespacedElementXml(prefix, localName, el) {
1037
1168
  const attrs = [];
1038
1169
  for (const [name, value] of Object.entries(el.attrs)) {
1039
- attrs.push(`${name}="${escapeAttr(value)}"`);
1170
+ attrs.push(`${name}="${escapeAttr2(value)}"`);
1040
1171
  }
1041
1172
  const head = attrs.length > 0 ? `<${prefix}:${localName} ${attrs.join(" ")}>` : `<${prefix}:${localName}>`;
1042
1173
  let body = "";
@@ -1056,7 +1187,7 @@ function metaAttrsXml(meta, namespaces) {
1056
1187
  const bucket = meta[prefix];
1057
1188
  if (!bucket?.attrs) continue;
1058
1189
  for (const [name, value] of Object.entries(bucket.attrs)) {
1059
- parts.push(`${prefix}:${name}="${escapeAttr(value)}"`);
1190
+ parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
1060
1191
  }
1061
1192
  }
1062
1193
  return parts.length > 0 ? ` ${parts.join(" ")}` : "";
@@ -1073,21 +1204,26 @@ function metaElementsXml(meta, namespaces) {
1073
1204
  }
1074
1205
  return out;
1075
1206
  }
1076
- function registerGradients(nodes, registry) {
1207
+ function registerPaintServers(nodes, registry) {
1077
1208
  for (const n of nodes) {
1078
1209
  if (n.kind === "group") {
1079
- registerGradients(n.children, registry);
1210
+ registerPaintServers(n.children, registry);
1080
1211
  } else if (n.kind === "path") {
1081
1212
  if (n.fill.kind === "gradient") registry.register(n.fill.paint);
1082
1213
  if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
1083
1214
  } else if (n.kind === "text") {
1084
- const styleFill = n.style?.fill;
1085
- if (styleFill && !("color" in styleFill)) {
1086
- registry.register(styleFill);
1215
+ registerTextPaint(n.style?.fill, registry);
1216
+ registerTextPaint(n.style?.stroke?.paint, registry);
1217
+ for (const run of n.runs ?? []) {
1218
+ registerTextPaint(run.fill, registry);
1219
+ registerTextPaint(run.stroke?.paint, registry);
1087
1220
  }
1088
1221
  }
1089
1222
  }
1090
1223
  }
1224
+ function registerTextPaint(paint, registry) {
1225
+ if (paint && !("color" in paint)) registry.register(paint);
1226
+ }
1091
1227
  function nodeXml(node, registry, namespaces) {
1092
1228
  if (node.kind === "group") return groupXml(node, registry, namespaces);
1093
1229
  if (node.kind === "text") return textXml(node, registry, namespaces);
@@ -1149,6 +1285,28 @@ function paintAttrs(paint, name, registry) {
1149
1285
  const id = registry.register(paint.paint);
1150
1286
  return [`${name}="url(#${id})"`];
1151
1287
  }
1288
+ function coreStrokeAttrs(stroke, registry) {
1289
+ if (!stroke) return [];
1290
+ const width = stroke.width ?? 1;
1291
+ if (!(width > 0)) return [];
1292
+ const attrs = [];
1293
+ if ("color" in stroke.paint) {
1294
+ attrs.push(`stroke="${stroke.paint.color}"`);
1295
+ if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
1296
+ attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
1297
+ }
1298
+ } else {
1299
+ attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
1300
+ }
1301
+ attrs.push(`stroke-width="${trimNumber(width)}"`);
1302
+ if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
1303
+ if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
1304
+ if (stroke.dash && stroke.dash.length > 0) {
1305
+ attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
1306
+ }
1307
+ if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
1308
+ return attrs;
1309
+ }
1152
1310
  function strokeAttrsFor(stroke, registry) {
1153
1311
  const attrs = paintAttrs(stroke.paint, "stroke", registry);
1154
1312
  attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
@@ -1198,7 +1356,7 @@ function textXml(node, registry, namespaces) {
1198
1356
  ];
1199
1357
  const style = node.style;
1200
1358
  if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(style.fontSize)}"`);
1201
- if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
1359
+ if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
1202
1360
  if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
1203
1361
  if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
1204
1362
  if (style?.align && style.align !== "left") {
@@ -1221,6 +1379,7 @@ function textXml(node, registry, namespaces) {
1221
1379
  attrs.push(`fill="url(#${id})"`);
1222
1380
  }
1223
1381
  }
1382
+ for (const a of coreStrokeAttrs(style?.stroke, registry)) attrs.push(a);
1224
1383
  if (node.opacity != null && node.opacity !== 1) {
1225
1384
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
1226
1385
  }
@@ -1239,7 +1398,7 @@ function runXml(run, registry) {
1239
1398
  const attrs = [];
1240
1399
  if (run.bold) attrs.push(`font-weight="700"`);
1241
1400
  if (run.italic) attrs.push(`font-style="italic"`);
1242
- if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
1401
+ if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
1243
1402
  if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
1244
1403
  if (run.letterSpacing != null) {
1245
1404
  attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
@@ -1254,6 +1413,7 @@ function runXml(run, registry) {
1254
1413
  attrs.push(`fill="url(#${id})"`);
1255
1414
  }
1256
1415
  }
1416
+ for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
1257
1417
  const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
1258
1418
  return `${head}${escapeText(run.text)}</tspan>`;
1259
1419
  }
@@ -1263,7 +1423,7 @@ function textDecorationValue(underline, strikethrough) {
1263
1423
  if (strikethrough) tokens.push("line-through");
1264
1424
  return tokens.length > 0 ? tokens.join(" ") : null;
1265
1425
  }
1266
- function escapeAttr(s) {
1426
+ function escapeAttr2(s) {
1267
1427
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1268
1428
  }
1269
1429
  function escapeText(s) {
@@ -1430,6 +1590,6 @@ async function unpackSvgFiles(files, ctx) {
1430
1590
  }
1431
1591
  }
1432
1592
 
1433
- export { IDENTITY_MATRIX, parseSvg, serializeSvg, svgNodesToKitDrafts, unpackSvgFiles };
1593
+ export { IDENTITY_MATRIX, parseSvg, serializeSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
1434
1594
  //# sourceMappingURL=index.js.map
1435
1595
  //# sourceMappingURL=index.js.map