@weasel-js/svg 1.1.0 → 1.3.0-pre.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, TilePatternSpec, IngestCtx } from '@weasel-js/core';
1
+ import { Path, FillStyle, StyledRun, TextStyle, Stroke, TilePatternSpec, IngestCtx } from '@weasel-js/core';
2
2
 
3
3
  /**
4
4
  * Public types for `@weasel-js/svg`. The package exposes a flat,
@@ -86,6 +86,10 @@ interface SvgStroke {
86
86
  * attribute may render with longer miters than the source SVG intended.
87
87
  */
88
88
  miterLimit?: number;
89
+ /** `marker-start` / `marker-mid` / `marker-end`, as the bare `url(#id)` key. */
90
+ markerStart?: string;
91
+ markerMid?: string;
92
+ markerEnd?: string;
89
93
  }
90
94
  /**
91
95
  * Leaf node: a path geometry plus fill/stroke. All other v1 shapes
@@ -155,8 +159,16 @@ interface SvgTextNode {
155
159
  text: string;
156
160
  /** Optional per-range styling — same shape as `TextPose.runs`. */
157
161
  runs?: StyledRun[];
158
- /** Node-wide style. Defaults applied at render time via `resolveTextStyle`. */
162
+ /** Node-wide typography. Defaults applied at render time via `resolveTextStyle`. */
159
163
  style?: TextStyle;
164
+ /**
165
+ * Node-wide glyph paint, the `FillStyle` / `Stroke` a kit text node holds
166
+ * in `data.fill` / `data.stroke`. Not `SvgPaint`, which is the path
167
+ * nodes' shape — a text paint reaches the renderer through the runs, and
168
+ * `StyledRun.fill` / `.stroke` override these per range.
169
+ */
170
+ fill?: FillStyle;
171
+ stroke?: Stroke;
160
172
  /** Element-level opacity (`opacity="..."`), 0..1. */
161
173
  opacity?: number;
162
174
  /** Element-level rotation in **radians**, pivoting around the unrotated
@@ -358,6 +370,14 @@ type SvgSceneDraft = {
358
370
  pose: DraftPose;
359
371
  data: Record<string, unknown>;
360
372
  };
373
+ /** Lower an `SvgStroke` onto the leaf's `data.stroke`.
374
+ *
375
+ * Everything the SVG carried — paint, width, cap, join, dash, miter limit,
376
+ * opacity — lands on the one `Stroke` `data.stroke` takes. The paint is
377
+ * normalized to the leaf's own box the same way a fill is, so a
378
+ * `userSpaceOnUse` gradient survives the fit-clamp and the drop-point
379
+ * placement. */
380
+ declare function strokeDataFromSvg(stroke: SvgStroke | undefined, box: SvgDraftBounds): Stroke | undefined;
361
381
  /**
362
382
  * Walk an `SvgNode[]` tree and emit a flat, parent-before-child list of
363
383
  * {@link SvgSceneDraft}s. Each `<g>` becomes a container whose pose is the
@@ -373,4 +393,4 @@ declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => st
373
393
  */
374
394
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
375
395
 
376
- export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
396
+ export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, PATH_C, PATH_Q, resolveTextStyle, fillToBoundsFrame } from '@weasel-js/core';
1
+ import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, getMarker, SCRIPT_METRICS, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, PATH_C, PATH_Q, resolveStrokeWidth, solid, fillToBoundsFrame, resolveTextStyle, getPaintKind } from '@weasel-js/core';
2
2
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
3
3
 
4
4
  // src/parse.ts
@@ -354,6 +354,39 @@ function readPattern(el, onWarn) {
354
354
  function escapeAttr(s) {
355
355
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
356
356
  }
357
+ function serializePathD(path) {
358
+ if (path.kind === "rect") {
359
+ const { x, y, width, height } = path;
360
+ return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
361
+ }
362
+ const cmds = path.commands;
363
+ const xs = path.coords;
364
+ const out = [];
365
+ let ci = 0;
366
+ for (let i = 0; i < cmds.length; i++) {
367
+ const c = cmds[i];
368
+ if (c === PATH_M) {
369
+ out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
370
+ ci += 2;
371
+ } else if (c === PATH_L) {
372
+ out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
373
+ ci += 2;
374
+ } else if (c === PATH_C) {
375
+ out.push(
376
+ `C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
377
+ );
378
+ ci += 6;
379
+ } else if (c === PATH_Q) {
380
+ out.push(
381
+ `Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
382
+ );
383
+ ci += 4;
384
+ } else if (c === PATH_Z) {
385
+ out.push("Z");
386
+ }
387
+ }
388
+ return out.join("");
389
+ }
357
390
 
358
391
  // src/gradients.ts
359
392
  var GRADIENT_TAGS = /* @__PURE__ */ new Set(["lineargradient", "radialgradient"]);
@@ -376,7 +409,7 @@ function warnUnsupportedDefsChildren(svg, onWarn) {
376
409
  for (let i = 0; i < root.children.length; i++) {
377
410
  const child = root.children[i];
378
411
  const tag = child.tagName.toLowerCase();
379
- if (GRADIENT_TAGS.has(tag) || tag === "pattern") continue;
412
+ if (GRADIENT_TAGS.has(tag) || tag === "pattern" || tag === "marker") continue;
380
413
  onWarn(`unsupported <defs> child: <${child.tagName}>`);
381
414
  }
382
415
  }
@@ -478,6 +511,13 @@ var PaintServerRegistry = class {
478
511
  byPaint = /* @__PURE__ */ new Map();
479
512
  order = [];
480
513
  counter = 0;
514
+ // Marker keys referenced by any stroke, in first-use order. The `<defs>`
515
+ // id is the marker key itself, not a minted counter id: `parseSvg` reads a
516
+ // `marker-end="url(#id)"` fragment back as the marker key directly (it
517
+ // never inspects the `<marker>` element), so a synthetic id would come
518
+ // back unresolvable and warn on round-trip.
519
+ markerKeys = [];
520
+ markerKeySet = /* @__PURE__ */ new Set();
481
521
  register(paint) {
482
522
  const existing = this.byPaint.get(paint);
483
523
  if (existing) return existing;
@@ -486,20 +526,50 @@ var PaintServerRegistry = class {
486
526
  this.order.push(paint);
487
527
  return id;
488
528
  }
489
- /** Emit `<defs>...</defs>` XML for every registered paint server. */
529
+ /** The `<defs>` id for a marker key — the key itself, minting nothing.
530
+ * `undefined` when nothing is registered under `key`, so a caller emits no
531
+ * attribute at all rather than a `url(#…)` pointing at a def that will
532
+ * never be written. */
533
+ markerId(key) {
534
+ if (getMarker(key) === void 0) return void 0;
535
+ if (!this.markerKeySet.has(key)) {
536
+ this.markerKeySet.add(key);
537
+ this.markerKeys.push(key);
538
+ }
539
+ return key;
540
+ }
541
+ /** Emit `<defs>...</defs>` XML for every registered paint server and marker. */
490
542
  toDefsXml(onWarn) {
491
- if (this.order.length === 0) return "";
543
+ if (this.order.length === 0 && this.markerKeys.length === 0) return "";
492
544
  const parts = ["<defs>"];
493
545
  for (const paint of this.order) {
494
- const id = this.byPaint.get(paint);
495
- parts.push(
496
- paint.fill === "pattern" ? patternXml(id, paint, onWarn) : gradientXml(id, paint)
497
- );
546
+ parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
547
+ }
548
+ for (const key of this.markerKeys) {
549
+ const entry = getMarker(key);
550
+ if (!entry) continue;
551
+ parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry));
498
552
  }
499
553
  parts.push("</defs>");
500
554
  return parts.join("");
501
555
  }
502
556
  };
557
+ function defaultMarkerXml(id, entry) {
558
+ const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
559
+ const d = serializePathD(path);
560
+ const fill = entry.fill === "none" ? "none" : "context-stroke";
561
+ const outline = entry.outline ? ` stroke="context-stroke" stroke-width="${entry.outline.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
562
+ return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="auto" overflow="visible"><path d="${d}" fill="${fill}"${outline}/></marker>`;
563
+ }
564
+ function paintServerXml(id, paint, onWarn) {
565
+ if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
566
+ const builtin = gradientXml(id, paint);
567
+ if (builtin) return builtin;
568
+ const custom = getPaintKind(paint.fill)?.toSvg?.(id, paint);
569
+ if (custom) return custom;
570
+ onWarn?.(`${paint.fill} fill has no vector form \u2014 omitted from <defs>`);
571
+ return "";
572
+ }
503
573
  function gradientUnitsAttr(units) {
504
574
  return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
505
575
  }
@@ -536,6 +606,9 @@ var INHERITABLE = [
536
606
  "stroke-linejoin",
537
607
  "stroke-dasharray",
538
608
  "stroke-miterlimit",
609
+ "marker-start",
610
+ "marker-mid",
611
+ "marker-end",
539
612
  "color",
540
613
  "font-size",
541
614
  "font-family",
@@ -543,7 +616,8 @@ var INHERITABLE = [
543
616
  "font-style",
544
617
  "text-anchor",
545
618
  "letter-spacing",
546
- "text-decoration"
619
+ "text-decoration",
620
+ "direction"
547
621
  ];
548
622
  function readStyleProp(el, prop) {
549
623
  const style = el.getAttribute("style");
@@ -588,6 +662,7 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
588
662
  "lineargradient",
589
663
  "radialgradient",
590
664
  "pattern",
665
+ "marker",
591
666
  "title",
592
667
  "desc",
593
668
  "metadata"
@@ -953,8 +1028,28 @@ function readStroke(style, gradients, onWarn) {
953
1028
  if (Number.isFinite(m) && m >= 1) stroke.miterLimit = m;
954
1029
  else onWarn(`unrecognized stroke-miterlimit: ${miterAttr}`);
955
1030
  }
1031
+ for (const [attr, field] of [
1032
+ ["marker-start", "markerStart"],
1033
+ ["marker-mid", "markerMid"],
1034
+ ["marker-end", "markerEnd"]
1035
+ ]) {
1036
+ const id = parseMarkerRef(style[attr] ?? null);
1037
+ if (id === void 0) continue;
1038
+ if (getMarker(id) === void 0) {
1039
+ onWarn(`${attr} references a marker this kit has no entry for: #${id}`);
1040
+ continue;
1041
+ }
1042
+ stroke[field] = id;
1043
+ }
956
1044
  return stroke;
957
1045
  }
1046
+ function parseMarkerRef(raw) {
1047
+ if (!raw) return void 0;
1048
+ const v = raw.trim();
1049
+ if (v === "" || v === "none") return void 0;
1050
+ const m = /^url\(\s*#([^)\s]+)\s*\)/.exec(v);
1051
+ return m ? m[1] : void 0;
1052
+ }
958
1053
  var STROKE_KEYS = [
959
1054
  "stroke",
960
1055
  "stroke-width",
@@ -962,7 +1057,10 @@ var STROKE_KEYS = [
962
1057
  "stroke-linecap",
963
1058
  "stroke-linejoin",
964
1059
  "stroke-dasharray",
965
- "stroke-miterlimit"
1060
+ "stroke-miterlimit",
1061
+ "marker-start",
1062
+ "marker-mid",
1063
+ "marker-end"
966
1064
  ];
967
1065
  function ownStrokeStyle(el) {
968
1066
  const out = {};
@@ -987,6 +1085,9 @@ function coreStroke(stroke) {
987
1085
  if (stroke.join) out.join = stroke.join;
988
1086
  if (stroke.dash) out.dash = stroke.dash;
989
1087
  if (stroke.miterLimit != null) out.miterLimit = stroke.miterLimit;
1088
+ if (stroke.markerStart) out.markerStart = stroke.markerStart;
1089
+ if (stroke.markerMid) out.markerMid = stroke.markerMid;
1090
+ if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
990
1091
  return out;
991
1092
  }
992
1093
  function parseDashArray(s) {
@@ -1024,8 +1125,31 @@ function parseTextDecoration(raw) {
1024
1125
  const out = {};
1025
1126
  if (tokens.includes("underline")) out.underline = true;
1026
1127
  if (tokens.includes("line-through")) out.strikethrough = true;
1128
+ if (tokens.includes("overline")) out.overline = true;
1027
1129
  return out;
1028
1130
  }
1131
+ function parseBaselineShift(raw, onWarn) {
1132
+ const keyword = raw.trim().toLowerCase();
1133
+ if (keyword === "super" || keyword === "sub") return { script: keyword };
1134
+ const n = parseFloat(raw);
1135
+ if (!Number.isFinite(n)) return {};
1136
+ const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
1137
+ if (unit === "%") return { baselineShift: n / 100 };
1138
+ if (unit && unit !== "em") {
1139
+ onWarn(`baseline-shift "${raw}" uses unit "${unit}", which is not converted; treated as ${n} em`);
1140
+ }
1141
+ return n === 0 ? {} : { baselineShift: n };
1142
+ }
1143
+ function parseRunFontSize(raw, onWarn) {
1144
+ const n = parseFloat(raw);
1145
+ if (!Number.isFinite(n)) return {};
1146
+ const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
1147
+ if (unit === "%") return { fontScale: n / 100 };
1148
+ if (unit && unit !== "px") {
1149
+ onWarn(`font-size "${raw}" uses unit "${unit}", which is not converted; treated as ${n} world units`);
1150
+ }
1151
+ return { fontSize: n };
1152
+ }
1029
1153
  var XLINK_NS2 = "http://www.w3.org/1999/xlink";
1030
1154
  function parseImageElement(el, ctm, onWarn) {
1031
1155
  const href = el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
@@ -1124,7 +1248,8 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1124
1248
  const ax = m[0] * rawX + m[2] * rawY + m[4];
1125
1249
  const ay = m[1] * rawX + m[3] * rawY + m[5];
1126
1250
  const leafStyle = deriveStyle(style, el);
1127
- const textStyle = readTextStyle(leafStyle, gradients, onWarn);
1251
+ const textStyle = readTextStyle(leafStyle, onWarn);
1252
+ const textPaint = readTextPaint(leafStyle, gradients, onWarn);
1128
1253
  const fontSize = textStyle.fontSize ?? 16;
1129
1254
  const lineHeight = textStyle.lineHeight ?? 1.2;
1130
1255
  const dominantBaseline = el.getAttribute("dominant-baseline");
@@ -1170,6 +1295,8 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1170
1295
  );
1171
1296
  if (hasStyling) node.runs = runs;
1172
1297
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
1298
+ if (textPaint.fill != null) node.fill = textPaint.fill;
1299
+ if (textPaint.stroke != null) node.stroke = textPaint.stroke;
1173
1300
  if (opacity != null) node.opacity = opacity;
1174
1301
  if (!isIdentity(localTransform)) {
1175
1302
  const worldLocal = rebaseTransform(ctm, localTransform);
@@ -1198,17 +1325,25 @@ function readTspanRun(el, gradients, style, onWarn) {
1198
1325
  if (ff) run.fontFamily = ff;
1199
1326
  const sz = ownProp(el, "font-size");
1200
1327
  if (sz != null) {
1201
- const n = parseFloat(sz);
1202
- if (Number.isFinite(n)) run.fontSize = n;
1328
+ const size = parseRunFontSize(sz, onWarn);
1329
+ if (size.fontSize != null) run.fontSize = size.fontSize;
1330
+ if (size.fontScale != null) run.fontScale = size.fontScale;
1203
1331
  }
1204
1332
  const ls = ownProp(el, "letter-spacing");
1205
1333
  if (ls != null) {
1206
1334
  const n = parseLetterSpacing(ls, onWarn);
1207
1335
  if (n != null) run.letterSpacing = n;
1208
1336
  }
1337
+ const bs = ownProp(el, "baseline-shift");
1338
+ if (bs != null) {
1339
+ const shift = parseBaselineShift(bs, onWarn);
1340
+ if (shift.script != null) run.script = shift.script;
1341
+ if (shift.baselineShift != null) run.baselineShift = shift.baselineShift;
1342
+ }
1209
1343
  const decoration = parseTextDecoration(ownProp(el, "text-decoration"));
1210
1344
  if (decoration.underline) run.underline = true;
1211
1345
  if (decoration.strikethrough) run.strikethrough = true;
1346
+ if (decoration.overline) run.overline = true;
1212
1347
  const tspanStyle = deriveStyle(style, el);
1213
1348
  const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
1214
1349
  if (fillAttr) {
@@ -1224,7 +1359,7 @@ function readTspanRun(el, gradients, style, onWarn) {
1224
1359
  if (stroke) run.stroke = stroke;
1225
1360
  return run;
1226
1361
  }
1227
- function readTextStyle(style, gradients, onWarn) {
1362
+ function readTextStyle(style, onWarn) {
1228
1363
  const out = {};
1229
1364
  const sz = style["font-size"];
1230
1365
  if (sz != null) {
@@ -1240,10 +1375,12 @@ function readTextStyle(style, gradients, onWarn) {
1240
1375
  }
1241
1376
  const fs = style["font-style"];
1242
1377
  if (fs === "italic" || fs === "normal") out.fontStyle = fs;
1243
- const anchor = style["text-anchor"];
1244
- if (anchor === "start") out.align = "left";
1378
+ if (style["direction"] === "rtl") out.direction = "rtl";
1379
+ const ltr = out.direction !== "rtl";
1380
+ const anchor = style["text-anchor"] ?? (ltr ? void 0 : "start");
1381
+ if (anchor === "start") out.align = ltr ? "left" : "right";
1245
1382
  else if (anchor === "middle") out.align = "center";
1246
- else if (anchor === "end") out.align = "right";
1383
+ else if (anchor === "end") out.align = ltr ? "right" : "left";
1247
1384
  const ls = style["letter-spacing"];
1248
1385
  if (ls != null) {
1249
1386
  const n = parseLetterSpacing(ls, onWarn);
@@ -1252,6 +1389,11 @@ function readTextStyle(style, gradients, onWarn) {
1252
1389
  const decoration = parseTextDecoration(style["text-decoration"] ?? null);
1253
1390
  if (decoration.underline) out.underline = true;
1254
1391
  if (decoration.strikethrough) out.strikethrough = true;
1392
+ if (decoration.overline) out.overline = true;
1393
+ return out;
1394
+ }
1395
+ function readTextPaint(style, gradients, onWarn) {
1396
+ const out = {};
1255
1397
  const fillRaw = resolveCurrentColor(style["fill"] ?? null, style);
1256
1398
  if (fillRaw) {
1257
1399
  const parsed = parsePaintAttr(fillRaw);
@@ -1266,41 +1408,6 @@ function readTextStyle(style, gradients, onWarn) {
1266
1408
  if (stroke) out.stroke = stroke;
1267
1409
  return out;
1268
1410
  }
1269
- function serializePathD(path) {
1270
- if (path.kind === "rect") {
1271
- const { x, y, width, height } = path;
1272
- return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
1273
- }
1274
- const cmds = path.commands;
1275
- const xs = path.coords;
1276
- const out = [];
1277
- let ci = 0;
1278
- for (let i = 0; i < cmds.length; i++) {
1279
- const c = cmds[i];
1280
- if (c === PATH_M) {
1281
- out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
1282
- ci += 2;
1283
- } else if (c === PATH_L) {
1284
- out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
1285
- ci += 2;
1286
- } else if (c === PATH_C) {
1287
- out.push(
1288
- `C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
1289
- );
1290
- ci += 6;
1291
- } else if (c === PATH_Q) {
1292
- out.push(
1293
- `Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
1294
- );
1295
- ci += 4;
1296
- } else if (c === PATH_Z) {
1297
- out.push("Z");
1298
- }
1299
- }
1300
- return out.join("");
1301
- }
1302
-
1303
- // src/serialize.ts
1304
1411
  function serializeSvg(nodes, opts = {}) {
1305
1412
  const registry = new PaintServerRegistry();
1306
1413
  registerPaintServers(nodes, registry);
@@ -1385,9 +1492,11 @@ function registerPaintServers(nodes, registry) {
1385
1492
  } else if (n.kind === "path") {
1386
1493
  if (n.fill.kind === "gradient") registry.register(n.fill.paint);
1387
1494
  if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
1495
+ registerMarkers(n.stroke, registry);
1388
1496
  } else if (n.kind === "text") {
1389
- registerTextPaint(n.style?.fill, registry);
1390
- registerTextPaint(n.style?.stroke?.paint, registry);
1497
+ registerTextPaint(n.fill, registry);
1498
+ registerTextPaint(n.stroke?.paint, registry);
1499
+ registerMarkers(n.stroke, registry);
1391
1500
  for (const run of n.runs ?? []) {
1392
1501
  registerTextPaint(run.fill, registry);
1393
1502
  registerTextPaint(run.stroke?.paint, registry);
@@ -1398,6 +1507,18 @@ function registerPaintServers(nodes, registry) {
1398
1507
  function registerTextPaint(paint, registry) {
1399
1508
  if (paint && !("color" in paint)) registry.register(paint);
1400
1509
  }
1510
+ function markerKeyOfRef(ref) {
1511
+ if (typeof ref === "string") return ref;
1512
+ if (ref && typeof ref === "object" && "key" in ref) return String(ref.key);
1513
+ return void 0;
1514
+ }
1515
+ function registerMarkers(stroke, registry) {
1516
+ if (!stroke) return;
1517
+ for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
1518
+ const key = markerKeyOfRef(ref);
1519
+ if (key) registry.markerId(key);
1520
+ }
1521
+ }
1401
1522
  function nodeXml(node, registry, namespaces) {
1402
1523
  if (node.kind === "group") return groupXml(node, registry, namespaces);
1403
1524
  if (node.kind === "text") return textXml(node, registry, namespaces);
@@ -1485,7 +1606,7 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
1485
1606
  }
1486
1607
  function coreStrokeAttrs(stroke, registry) {
1487
1608
  if (!stroke) return [];
1488
- const width = stroke.width ?? 1;
1609
+ const width = resolveStrokeWidth(stroke.width ?? 1, 1);
1489
1610
  if (!(width > 0)) return [];
1490
1611
  const attrs = [];
1491
1612
  if ("color" in stroke.paint) {
@@ -1503,6 +1624,18 @@ function coreStrokeAttrs(stroke, registry) {
1503
1624
  attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
1504
1625
  }
1505
1626
  if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
1627
+ for (const [field, attr] of [
1628
+ ["markerStart", "marker-start"],
1629
+ ["markerMid", "marker-mid"],
1630
+ ["markerEnd", "marker-end"]
1631
+ ]) {
1632
+ const ref = stroke[field];
1633
+ if (ref === void 0) continue;
1634
+ const key = typeof ref === "string" ? ref : ref.key;
1635
+ const id = registry.markerId(key);
1636
+ if (id === void 0) continue;
1637
+ attrs.push(`${attr}="url(#${id})"`);
1638
+ }
1506
1639
  return attrs;
1507
1640
  }
1508
1641
  function strokeAttrsFor(stroke, registry) {
@@ -1524,6 +1657,17 @@ function strokeAttrsFor(stroke, registry) {
1524
1657
  if (stroke.miterLimit != null) {
1525
1658
  attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
1526
1659
  }
1660
+ for (const [field, attr] of [
1661
+ ["markerStart", "marker-start"],
1662
+ ["markerMid", "marker-mid"],
1663
+ ["markerEnd", "marker-end"]
1664
+ ]) {
1665
+ const ref = stroke[field];
1666
+ if (ref === void 0) continue;
1667
+ const id = registry.markerId(ref);
1668
+ if (id === void 0) continue;
1669
+ attrs.push(`${attr}="url(#${id})"`);
1670
+ }
1527
1671
  return attrs;
1528
1672
  }
1529
1673
  function computeBounds(nodes) {
@@ -1577,27 +1721,30 @@ function textXml(node, registry, namespaces) {
1577
1721
  if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
1578
1722
  if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
1579
1723
  if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
1580
- if (style?.align && style.align !== "left") {
1581
- const anchor = style.align === "center" ? "middle" : "end";
1582
- attrs.push(`text-anchor="${anchor}"`);
1724
+ const direction = style?.direction ?? "ltr";
1725
+ if (direction === "rtl") attrs.push('direction="rtl"');
1726
+ if (style?.align != null || direction === "rtl") {
1727
+ const align = style?.align ?? "left";
1728
+ const anchor = align === "center" ? "middle" : align === "start" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
1729
+ if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
1583
1730
  }
1584
1731
  if (style?.letterSpacing != null && style.letterSpacing !== 0) {
1585
1732
  attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
1586
1733
  }
1587
- const decoration = textDecorationValue(style?.underline, style?.strikethrough);
1734
+ const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
1588
1735
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
1589
- if (style?.fill) {
1590
- if ("color" in style.fill) {
1591
- attrs.push(`fill="${style.fill.color}"`);
1592
- if (style.fill.opacity != null && style.fill.opacity !== 1) {
1593
- attrs.push(`fill-opacity="${trimNumber(style.fill.opacity)}"`);
1736
+ if (node.fill) {
1737
+ if ("color" in node.fill) {
1738
+ attrs.push(`fill="${node.fill.color}"`);
1739
+ if (node.fill.opacity != null && node.fill.opacity !== 1) {
1740
+ attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
1594
1741
  }
1595
1742
  } else {
1596
- const id = registry.register(style.fill);
1743
+ const id = registry.register(node.fill);
1597
1744
  attrs.push(`fill="url(#${id})"`);
1598
1745
  }
1599
1746
  }
1600
- for (const a of coreStrokeAttrs(style?.stroke, registry)) attrs.push(a);
1747
+ for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
1601
1748
  if (node.opacity != null && node.opacity !== 1) {
1602
1749
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
1603
1750
  }
@@ -1617,11 +1764,19 @@ function runXml(run, registry) {
1617
1764
  if (run.bold) attrs.push(`font-weight="700"`);
1618
1765
  if (run.italic) attrs.push(`font-style="italic"`);
1619
1766
  if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
1767
+ const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
1768
+ const scale = run.fontScale ?? presetScale;
1620
1769
  if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
1770
+ else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
1771
+ if (run.baselineShift != null) {
1772
+ attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
1773
+ } else if (run.script) {
1774
+ attrs.push(`baseline-shift="${run.script}"`);
1775
+ }
1621
1776
  if (run.letterSpacing != null) {
1622
1777
  attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
1623
1778
  }
1624
- const runDecoration = textDecorationValue(run.underline, run.strikethrough);
1779
+ const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
1625
1780
  if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
1626
1781
  if (run.fill) {
1627
1782
  if ("color" in run.fill) {
@@ -1635,10 +1790,11 @@ function runXml(run, registry) {
1635
1790
  const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
1636
1791
  return `${head}${escapeText(run.text)}</tspan>`;
1637
1792
  }
1638
- function textDecorationValue(underline, strikethrough) {
1793
+ function textDecorationValue(underline, strikethrough, overline) {
1639
1794
  const tokens = [];
1640
1795
  if (underline) tokens.push("underline");
1641
1796
  if (strikethrough) tokens.push("line-through");
1797
+ if (overline) tokens.push("overline");
1642
1798
  return tokens.length > 0 ? tokens.join(" ") : null;
1643
1799
  }
1644
1800
  function escapeAttr2(s) {
@@ -1661,7 +1817,6 @@ function pathBounds(path) {
1661
1817
  }
1662
1818
  var CASCADE_OFFSET_PX = 24;
1663
1819
  var VIEWPORT_FIT = 0.9;
1664
- var GRADIENT_FALLBACK = "#888888";
1665
1820
  function readFileText(file) {
1666
1821
  return new Promise((resolve, reject) => {
1667
1822
  const reader = new FileReader();
@@ -1676,18 +1831,27 @@ function freshSvgNodeId() {
1676
1831
  }
1677
1832
  function fillFromPaint(paint, box) {
1678
1833
  if (!paint) return void 0;
1679
- if (paint.kind === "none") return "none";
1680
- if (paint.kind === "solid") return paint.color;
1834
+ if (paint.kind === "none") return null;
1835
+ if (paint.kind === "solid") return solid(paint.color);
1681
1836
  const g = paint.paint;
1682
1837
  const units = "units" in g ? g.units : void 0;
1683
1838
  return units === "world" ? fillToBoundsFrame(g, box) : g;
1684
1839
  }
1685
- function strokeColorFromPaint(paint) {
1686
- if (!paint) return void 0;
1687
- if (paint.kind === "none") return "none";
1688
- if (paint.kind === "solid") return paint.color;
1689
- dwarn("ingest", `svg unpack: gradient stroke flattened to ${GRADIENT_FALLBACK}`);
1690
- return GRADIENT_FALLBACK;
1840
+ function strokeDataFromSvg(stroke, box) {
1841
+ if (!stroke || stroke.paint.kind === "none") return void 0;
1842
+ const paint = fillFromPaint(stroke.paint, box);
1843
+ if (paint === void 0 || paint === null) return void 0;
1844
+ return {
1845
+ paint: stroke.opacity !== void 0 ? { ...paint, opacity: stroke.opacity } : paint,
1846
+ width: stroke.width,
1847
+ ...stroke.cap !== void 0 ? { cap: stroke.cap } : {},
1848
+ ...stroke.join !== void 0 ? { join: stroke.join } : {},
1849
+ ...stroke.dash !== void 0 ? { dash: stroke.dash } : {},
1850
+ ...stroke.miterLimit !== void 0 ? { miterLimit: stroke.miterLimit } : {},
1851
+ ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
1852
+ ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
1853
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
1854
+ };
1691
1855
  }
1692
1856
  function svgNodesToKitDrafts(nodes, nextId) {
1693
1857
  const drafts = [];
@@ -1750,7 +1914,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
1750
1914
  const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
1751
1915
  if (n.rotation) pose.rotation = n.rotation;
1752
1916
  const fill = fillFromPaint(n.fill, b);
1753
- const stroke = n.stroke ? strokeColorFromPaint(n.stroke.paint) : void 0;
1917
+ const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
1754
1918
  drafts.push({
1755
1919
  kind: "leaf",
1756
1920
  id: nextId(),
@@ -1759,7 +1923,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
1759
1923
  data: {
1760
1924
  path: n.path,
1761
1925
  ...fill !== void 0 ? { fill } : {},
1762
- ...stroke !== void 0 && stroke !== "none" ? { stroke, strokeWidth: n.stroke.width } : {}
1926
+ ...strokeFromSvg !== void 0 ? { stroke: strokeFromSvg } : {}
1763
1927
  }
1764
1928
  });
1765
1929
  return pose;
@@ -1853,6 +2017,6 @@ async function unpackSvgFiles(files, ctx) {
1853
2017
  }
1854
2018
  }
1855
2019
 
1856
- export { IDENTITY_MATRIX, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2020
+ export { IDENTITY_MATRIX, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
1857
2021
  //# sourceMappingURL=index.js.map
1858
2022
  //# sourceMappingURL=index.js.map