@weasel-js/svg 1.10.0 → 1.11.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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { identity, multiply, invert, fillsOpenSubpaths, splitByClosure, splitSubpaths, rotateAboutPoint, hasOpenSubpath } from '@weasel-js/geom';
2
- import { isTileKind, boundsOfPath, transformPath, isSolidPaint, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, pathThroughPoseFrame, DEFAULT_TEXT_STYLE, pathFromD, isPatternPaint, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, paragraphDirection, resolveRunFace, resolveTextStyle, parseColor, rgbaToHex, layoutRuns, resolveRuns, PathBuilder, getPaintKind, resolveMarkerSize, isTilePaint, tileSpecOf } from '@weasel-js/core';
1
+ import { identity, multiply, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
2
+ import { resolveCorners, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, pathThroughPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, DEFAULT_TEXT_STYLE, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, strokeToPath, toRuns, resolveRunFace, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
4
 
5
5
  // src/types.ts
@@ -177,9 +177,11 @@ function scaleLength(v, scale) {
177
177
  return typeof v === "number" ? v * scale : v;
178
178
  }
179
179
  function scaleStroke(stroke, scale) {
180
- const { width: width2 } = stroke;
181
- if (scale === 1 || typeof width2 === "object") return stroke;
182
- const out = { ...stroke };
180
+ const { width: width2, vertexWidths } = stroke;
181
+ if (scale === 1) return stroke;
182
+ const scaledVertices = vertexWidths ? { vertexWidths: vertexWidths.map((n) => scaleVertexWidth(n, scale)) } : {};
183
+ if (typeof width2 === "object") return vertexWidths ? { ...stroke, ...scaledVertices } : stroke;
184
+ const out = { ...stroke, ...scaledVertices };
183
185
  if (width2 !== void 0) out.width = width2 * scale;
184
186
  if (out.dash) out.dash = out.dash.map((n) => n * scale);
185
187
  return out;
@@ -1381,7 +1383,8 @@ function readPattern(el, onWarn) {
1381
1383
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1382
1384
  return paint;
1383
1385
  }
1384
- function serializePathD(path) {
1386
+ function serializePathD(given) {
1387
+ const path = resolveCorners(given);
1385
1388
  if (path.kind === "rect") {
1386
1389
  const { x, y, width: width2, height: height2 } = path;
1387
1390
  return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width2)}v${trimNumber(height2)}h${trimNumber(-width2)}Z`;
@@ -1548,9 +1551,9 @@ function warnUnmodeled(el, elements, onWarn) {
1548
1551
  break;
1549
1552
  }
1550
1553
  }
1551
- const spread = inheritedAttr(el, elements, "spreadMethod");
1552
- if (spread === "reflect" || spread === "repeat") {
1553
- onWarn?.(`spreadMethod="${spread}" is not modeled; the gradient pads`);
1554
+ const spread2 = inheritedAttr(el, elements, "spreadMethod");
1555
+ if (spread2 === "reflect" || spread2 === "repeat") {
1556
+ onWarn?.(`spreadMethod="${spread2}" is not modeled; the gradient pads`);
1554
1557
  }
1555
1558
  }
1556
1559
  function coord(el, elements, name, axis, fraction, units, onWarn) {
@@ -2096,6 +2099,97 @@ function coreStroke(stroke) {
2096
2099
  if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
2097
2100
  return out;
2098
2101
  }
2102
+ var VERTEX_WIDTHS = "vertex-widths";
2103
+ var NIB = "nib";
2104
+ var WIDTH = "stroke-width";
2105
+ var ROLE = "role";
2106
+ var OUTLINE = "stroke-outline";
2107
+ var GROUP = "stroke-group";
2108
+ var MARKER_ATTR = /^(marker-(?:start|mid|end))=/;
2109
+ var UNSET = "-";
2110
+ var SIDES = "/";
2111
+ function attrOf(el, name) {
2112
+ return el.getAttributeNS(WEASEL_NS, name) ?? el.getAttribute(`${WEASEL_NS_PREFIX}:${name}`);
2113
+ }
2114
+ function hasVariableWidth(stroke) {
2115
+ return (stroke?.vertexWidths?.length ?? 0) > 0 || stroke?.nib !== void 0;
2116
+ }
2117
+ function writesVariableWidth(nodes) {
2118
+ return nodes.some((n) => n.kind === "group" ? writesVariableWidth(n.children) : n.kind === "path" && hasVariableWidth(n.stroke));
2119
+ }
2120
+ function widthToken(entry) {
2121
+ const { left, right } = vertexHalfWidths(entry, NaN);
2122
+ const side = (v) => Number.isNaN(v) ? UNSET : trimNumber(v);
2123
+ if (typeof entry === "number") return Number.isNaN(left) ? UNSET : trimNumber(left + right);
2124
+ return Number.isNaN(left) && Number.isNaN(right) ? UNSET : `${side(left)}${SIDES}${side(right)}`;
2125
+ }
2126
+ function widthOfToken(token) {
2127
+ const side = (v) => v === UNSET ? NaN : Number(v);
2128
+ if (!token.includes(SIDES)) return side(token);
2129
+ const [left, right] = token.split(SIDES);
2130
+ return { left: side(left), right: side(right) };
2131
+ }
2132
+ function variableWidthAttrs(stroke, strokeAttrs) {
2133
+ const width2 = typeof stroke.width === "number" ? stroke.width : stroke.width.px;
2134
+ const out = [
2135
+ ...strokeAttrs.map((a) => a.startsWith("stroke-width=") ? 'stroke-width="0"' : MARKER_ATTR.test(a) ? `${WEASEL_NS_PREFIX}:${a}` : a),
2136
+ `${WEASEL_NS_PREFIX}:${WIDTH}="${trimNumber(width2)}"`
2137
+ ];
2138
+ if (stroke.vertexWidths?.length) {
2139
+ const list = stroke.vertexWidths.map(widthToken).join(" ");
2140
+ out.push(`${WEASEL_NS_PREFIX}:${VERTEX_WIDTHS}="${list}"`);
2141
+ }
2142
+ if (stroke.nib) {
2143
+ out.push(`${WEASEL_NS_PREFIX}:${NIB}="${String(stroke.nib.angle)} ${String(stroke.nib.ratio)}"`);
2144
+ }
2145
+ return out;
2146
+ }
2147
+ function strokeOutlineXml(path, stroke, fillAttrs, transform) {
2148
+ const kit = {
2149
+ width: stroke.width,
2150
+ ...stroke.vertexWidths ? { vertexWidths: stroke.vertexWidths } : {},
2151
+ ...stroke.nib ? { nib: stroke.nib } : {},
2152
+ ...stroke.cap ? { cap: stroke.cap } : {},
2153
+ ...stroke.join ? { join: stroke.join } : {},
2154
+ ...stroke.dash ? { dash: stroke.dash } : {},
2155
+ ...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {},
2156
+ ...stroke.align ? { align: stroke.align } : {},
2157
+ ...stroke.markerStart ? { markerStart: stroke.markerStart } : {},
2158
+ ...stroke.markerMid ? { markerMid: stroke.markerMid } : {},
2159
+ ...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {}
2160
+ };
2161
+ const outline = strokeToPath(path, kit);
2162
+ const attrs = [`d="${serializePathD(outline)}"`, ...fillAttrs, 'stroke="none"'];
2163
+ if (transform) attrs.push(transform);
2164
+ attrs.push(`${WEASEL_NS_PREFIX}:${ROLE}="${OUTLINE}"`);
2165
+ return `<path ${attrs.join(" ")}/>`;
2166
+ }
2167
+ function strokeGroupXml(opacity, inner) {
2168
+ return `<g ${opacity} ${WEASEL_NS_PREFIX}:${ROLE}="${GROUP}">${inner}</g>`;
2169
+ }
2170
+ function unwrapStrokeGroup(el, node) {
2171
+ if (attrOf(el, ROLE) !== GROUP || !node || Array.isArray(node) || node.kind !== "group") return node;
2172
+ const [only] = node.children;
2173
+ if (node.children.length !== 1 || only.kind !== "path") return node;
2174
+ return node.opacity != null ? { ...only, opacity: node.opacity * (only.opacity ?? 1) } : only;
2175
+ }
2176
+ function isStrokeOutline(el) {
2177
+ return attrOf(el, ROLE) === OUTLINE;
2178
+ }
2179
+ function withVariableWidth(el, stroke) {
2180
+ const width2 = parseFloat(attrOf(el, WIDTH) ?? "");
2181
+ if (!stroke || !Number.isFinite(width2)) return stroke;
2182
+ const out = { ...stroke, width: typeof stroke.width === "number" ? width2 : { px: width2 } };
2183
+ const list = attrOf(el, VERTEX_WIDTHS);
2184
+ if (list) out.vertexWidths = list.trim().split(/\s+/).map(widthOfToken);
2185
+ const [angle, ratio] = (attrOf(el, NIB) ?? "").trim().split(/\s+/).map(Number);
2186
+ if (Number.isFinite(angle) && Number.isFinite(ratio)) out.nib = { angle, ratio };
2187
+ for (const [attr, field] of [["marker-start", "markerStart"], ["marker-mid", "markerMid"], ["marker-end", "markerEnd"]]) {
2188
+ const id = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attrOf(el, attr)?.trim() ?? "")?.[1];
2189
+ if (id) out[field] = id;
2190
+ }
2191
+ return out;
2192
+ }
2099
2193
  var near = (v, target) => Math.abs(v - target) <= 1e-9;
2100
2194
  function placeBox(box, m, stretch) {
2101
2195
  const [a, b, c, d] = m;
@@ -2238,6 +2332,32 @@ function anchorOffset(style, width2, text) {
2238
2332
  }
2239
2333
 
2240
2334
  // src/tspanLines.ts
2335
+ var LINES_ATTR = "data-weasel-lines";
2336
+ var LINE_HEIGHT_ATTR = "data-weasel-line-height";
2337
+ var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
2338
+ function linesXml(pieces, x, pitch) {
2339
+ let out = "";
2340
+ let pending = 0;
2341
+ let wrote = false;
2342
+ const tspan = (attrs, text) => `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
2343
+ const lineAttrs = () => {
2344
+ if (pending === 0) return [];
2345
+ const attrs = [`x="${trimNumber(x)}"`, `dy="${trimNumber(pending * pitch)}"`];
2346
+ if (pending > 1 || !wrote) attrs.push(`${LINES_ATTR}="${pending}"`);
2347
+ pending = 0;
2348
+ return attrs;
2349
+ };
2350
+ for (const piece of pieces) {
2351
+ piece.text.split(/[\n\u000B\u000C]/).forEach((line, i) => {
2352
+ if (i > 0) pending++;
2353
+ if (line === "") return;
2354
+ const attrs = [...piece.attrs ?? [], ...lineAttrs()];
2355
+ out += piece.attrs === null && attrs.length === 0 ? escapeText(line) : tspan(attrs, line);
2356
+ wrote = true;
2357
+ });
2358
+ }
2359
+ return pending > 0 ? out + tspan(lineAttrs(), "") : out;
2360
+ }
2241
2361
  var DEFAULT_LINE_HEIGHT = 1.2;
2242
2362
  var LineTracker = class {
2243
2363
  constructor(text, onWarn) {
@@ -2250,9 +2370,9 @@ var LineTracker = class {
2250
2370
  /** Where the current line sits, in user units. */
2251
2371
  at;
2252
2372
  pitches = [];
2253
- /** Whether `tspan` opens a new line. Call once per tspan, in order.
2254
- * `afterText` is whether any text precedes it: the first line's own
2255
- * placement is not a pitch. */
2373
+ /** How many lines down `tspan` opens a new line, or 0 when it stays on the
2374
+ * current one. Call once per tspan, in order. `afterText` is whether any
2375
+ * text precedes it: the first line's own placement is not a pitch. */
2256
2376
  startsLine(tspan, afterText) {
2257
2377
  const y = tspan.getAttribute("y");
2258
2378
  const movedY = y != null && y !== this.y;
@@ -2260,10 +2380,11 @@ var LineTracker = class {
2260
2380
  const from = this.at;
2261
2381
  const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2262
2382
  this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2263
- if (!tspan.hasAttribute("x")) return false;
2264
- const starts = movedY || dy !== 0;
2265
- if (starts && afterText && this.at > from) this.pitches.push(this.at - from);
2266
- return starts;
2383
+ if (!tspan.hasAttribute("x") || !(movedY || dy !== 0)) return 0;
2384
+ const stated = Number(tspan.getAttribute(LINES_ATTR) ?? NaN);
2385
+ const lines = Number.isInteger(stated) && stated >= 0 ? stated : 1;
2386
+ if (afterText && lines > 0 && this.at > from) this.pitches.push((this.at - from) / lines);
2387
+ return lines;
2267
2388
  }
2268
2389
  /**
2269
2390
  * The line height the lines were pitched at, as a multiple of `fontSize`:
@@ -2278,7 +2399,7 @@ var LineTracker = class {
2278
2399
  if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
2279
2400
  this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
2280
2401
  }
2281
- const lineHeight = mean / fontSize;
2402
+ const lineHeight = Math.round(mean / fontSize * 1e6) / 1e6;
2282
2403
  return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2283
2404
  }
2284
2405
  };
@@ -2288,20 +2409,33 @@ function warnUnreadPositions(el, onWarn) {
2288
2409
  const raw = el.getAttribute(name)?.trim() ?? "";
2289
2410
  if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2290
2411
  }
2291
- for (const name of ["dx", "rotate"]) {
2412
+ for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
2292
2413
  const raw = el.getAttribute(name)?.trim() ?? "";
2293
2414
  if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2294
2415
  onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
2295
2416
  }
2296
2417
  }
2297
2418
  }
2298
- function joinLines(lines) {
2419
+ function sameStyle(a, b) {
2420
+ const style = (run) => JSON.stringify(
2421
+ Object.entries(run).filter(([k, v]) => k !== "text" && v !== void 0).sort(([j], [k]) => j < k ? -1 : 1)
2422
+ );
2423
+ return style(a) === style(b);
2424
+ }
2425
+ function joinLines(lines, leading = 0) {
2299
2426
  const out = [];
2300
- let breaks = 0;
2427
+ let breaks = leading;
2301
2428
  for (const line of lines) {
2302
2429
  if (line.length > 0) {
2303
- if (breaks > 0) line[0] = { ...line[0], text: "\n".repeat(breaks) + line[0].text };
2304
- out.push(...line);
2430
+ const [first, ...rest] = line;
2431
+ const head = breaks > 0 ? { ...first, text: "\n".repeat(breaks) + first.text } : first;
2432
+ const last = out[out.length - 1];
2433
+ if (breaks > 0 && last && sameStyle(last, head)) {
2434
+ out[out.length - 1] = { ...last, text: last.text + head.text };
2435
+ } else {
2436
+ out.push(head);
2437
+ }
2438
+ out.push(...rest);
2305
2439
  breaks = 0;
2306
2440
  }
2307
2441
  if (out.length > 0) breaks++;
@@ -2309,14 +2443,124 @@ function joinLines(lines) {
2309
2443
  return out;
2310
2444
  }
2311
2445
 
2312
- // src/fillPaint.ts
2313
- function mapFillPaint(fill, f) {
2314
- if (!fill.paint) return fill;
2315
- const paint = f(fill.paint);
2316
- return paint === fill.paint ? fill : { ...fill, paint };
2446
+ // src/laidLines.ts
2447
+ var LAID_ATTR = "data-weasel-layout";
2448
+ function layoutMeasured(layout, text) {
2449
+ return layout.laid.bounds.width > 0 || text.trim() === "";
2450
+ }
2451
+ function anchorX(cells, { anchor, rtl }) {
2452
+ const left = Math.min(...cells.map((c) => c.x));
2453
+ const right = Math.max(...cells.map((c) => c.x + c.advance));
2454
+ if (anchor === "middle") return (left + right) / 2;
2455
+ return anchor === "start" === !rtl ? left : right;
2456
+ }
2457
+ function wordStarts(text, start, end) {
2458
+ const out = [start];
2459
+ for (let i = start + 1; i < end; i++) {
2460
+ if (!/\s/.test(text[i]) && /\s/.test(text[i - 1])) out.push(i);
2461
+ }
2462
+ return out;
2463
+ }
2464
+ function rangeXml(pieces, offsets, start, end, lead) {
2465
+ let out = "";
2466
+ let first = true;
2467
+ pieces.forEach((piece, k) => {
2468
+ const from = Math.max(start, offsets[k]);
2469
+ const to = Math.min(end, offsets[k] + piece.text.length);
2470
+ if (from >= to) return;
2471
+ const text = piece.text.slice(from - offsets[k], to - offsets[k]);
2472
+ const attrs = [...piece.attrs ?? [], ...first ? lead : []];
2473
+ first = false;
2474
+ out += piece.attrs === null && attrs.length === 0 ? escapeText(text) : `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
2475
+ });
2476
+ return out;
2477
+ }
2478
+ var HARD_BREAKS = /\r\n|[\n\v\f\r\u0085\u2028\u2029]/g;
2479
+ var breaksIn = (text) => text.match(HARD_BREAKS)?.length ?? 0;
2480
+ function laidLinesXml(pieces, layout, anchor) {
2481
+ const text = pieces.map((p) => p.text).join("");
2482
+ const offsets = [];
2483
+ pieces.reduce((at, p) => {
2484
+ offsets.push(at);
2485
+ return at + p.text.length;
2486
+ }, 0);
2487
+ const px = (x) => `x="${trimNumber(layout.x + x)}"`;
2488
+ const py = (line) => `y="${trimNumber(layout.y + line.baselineY)}"`;
2489
+ let out = "";
2490
+ let wrote = false;
2491
+ let prevEnd = 0;
2492
+ const count = (lines) => {
2493
+ const stated = lines !== 1 || !wrote ? [`${LINES_ATTR}="${lines}"`] : [];
2494
+ wrote = true;
2495
+ return stated;
2496
+ };
2497
+ for (const line of layout.laid.lines) {
2498
+ if (line.cells.length === 0) continue;
2499
+ const start = Math.min(...line.cells.map((c) => c.srcIndex));
2500
+ const stepped = breaksIn(text.slice(prevEnd, start));
2501
+ prevEnd = line.srcEnd;
2502
+ const starts = line.justified ? wordStarts(text, start, line.srcEnd) : [start];
2503
+ starts.forEach((from, k) => {
2504
+ const to = starts[k + 1] ?? line.srcEnd;
2505
+ const cells = line.cells.filter((c) => c.srcIndex >= from && c.srcIndex < to);
2506
+ const lead = [px(anchorX(cells, anchor)), ...k === 0 ? [py(line), ...count(stepped)] : []];
2507
+ out += rangeXml(pieces, offsets, from, to, lead);
2508
+ });
2509
+ }
2510
+ const trailing = breaksIn(text.slice(prevEnd));
2511
+ const last = layout.laid.lines[layout.laid.lines.length - 1];
2512
+ if (trailing > 0 && last) out += `<tspan ${[px(last.x0), py(last), ...count(trailing)].join(" ")}></tspan>`;
2513
+ return out;
2514
+ }
2515
+ var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
2516
+ function readDx(el, onWarn) {
2517
+ const raw = el.getAttribute("dx")?.trim();
2518
+ if (!raw) return null;
2519
+ const values = raw.split(/[\s,]+/).map((v) => resolveLength(v, el, "dx", "x", 0, onWarn));
2520
+ return values.some((v) => v !== 0) ? values : null;
2521
+ }
2522
+ var spacingOf = (run, inherited2) => resolveScreenLength(run.letterSpacing ?? inherited2 ?? 0, 1);
2523
+ function spread(run, extra, inherited2) {
2524
+ const base = spacingOf(run, inherited2);
2525
+ const out = [];
2526
+ let last;
2527
+ Array.from(run.text).forEach((glyph, i) => {
2528
+ const more = extra[i] ?? 0;
2529
+ if (out.length > 0 && more === last) out[out.length - 1].text += glyph;
2530
+ else out.push(more === 0 ? { ...run, text: glyph } : { ...run, text: glyph, letterSpacing: base + more });
2531
+ last = more;
2532
+ });
2533
+ return out;
2534
+ }
2535
+ function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
2536
+ const line = lines[lines.length - 1];
2537
+ const dx = readDx(tspan, onWarn);
2538
+ if (!dx) {
2539
+ line.push(run);
2540
+ return 0;
2541
+ }
2542
+ const raw = tspan.getAttribute("dx");
2543
+ if (COLLAPSIBLE.test(run.text)) {
2544
+ onWarn(`<tspan dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
2545
+ line.push(run);
2546
+ return 0;
2547
+ }
2548
+ let lead = 0;
2549
+ let before = line.length - 1;
2550
+ while (before >= 0 && line[before].text === "") before--;
2551
+ if (dx[0] !== 0 && before >= 0) {
2552
+ const glyphs = Array.from(line[before].text);
2553
+ line.splice(before, 1, ...spread(line[before], [...glyphs.slice(1).map(() => 0), dx[0]], inherited2));
2554
+ } else if (dx[0] !== 0 && lines.length === 1) {
2555
+ lead = dx[0];
2556
+ } else if (dx[0] !== 0) {
2557
+ onWarn(`<tspan dx="${raw}"> opens a line; the line starts at the box edge, and the rest of the list is read`);
2558
+ }
2559
+ line.push(...spread(run, dx.slice(1), inherited2));
2560
+ return lead;
2317
2561
  }
2318
2562
 
2319
- // src/readText.ts
2563
+ // src/textWhitespace.ts
2320
2564
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2321
2565
  function preservesSpace(el) {
2322
2566
  for (let e = el; e; e = e.parentElement) {
@@ -2353,6 +2597,15 @@ function collapseRunWhitespace(runs) {
2353
2597
  if (runs[i].text === "") runs.splice(i, 1);
2354
2598
  }
2355
2599
  }
2600
+
2601
+ // src/fillPaint.ts
2602
+ function mapFillPaint(fill, f) {
2603
+ if (!fill.paint) return fill;
2604
+ const paint = f(fill.paint);
2605
+ return paint === fill.paint ? fill : { ...fill, paint };
2606
+ }
2607
+
2608
+ // src/readText.ts
2356
2609
  var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
2357
2610
  function laidOutWidth(runs, style, plain) {
2358
2611
  const resolved = resolveTextStyle(style);
@@ -2367,6 +2620,10 @@ function laidOutWidth(runs, style, plain) {
2367
2620
  const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
2368
2621
  return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
2369
2622
  }
2623
+ function numberAttr(el, name) {
2624
+ const n = parseFloat(el.getAttribute(name) ?? "");
2625
+ return Number.isFinite(n) ? n : void 0;
2626
+ }
2370
2627
  function placeTextNode(node, m, onWarn) {
2371
2628
  const cx = node.x + node.width / 2;
2372
2629
  const cy = node.y + node.height / 2;
@@ -2408,38 +2665,14 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2408
2665
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2409
2666
  const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
2410
2667
  const dominantBaseline = el.getAttribute("dominant-baseline");
2411
- const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2668
+ const laid = el.hasAttribute(LAID_ATTR);
2669
+ const explicitTopAnchor = laid || dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2412
2670
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2413
2671
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2414
- const runLines = [[]];
2415
2672
  const tracker = new LineTracker(el, onWarn);
2416
- const push = (run) => {
2417
- runLines[runLines.length - 1].push(run);
2418
- };
2419
- for (let i = 0; i < el.childNodes.length; i++) {
2420
- const child = el.childNodes[i];
2421
- if (child.nodeType === 3) {
2422
- const t = child.textContent ?? "";
2423
- if (!t) continue;
2424
- push({ text: t, ...inheritedShift });
2425
- } else if (child.nodeType === 1) {
2426
- const sp = child;
2427
- if (sp.tagName.toLowerCase() !== "tspan") {
2428
- onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2429
- const t = sp.textContent ?? "";
2430
- if (t) push({ text: t, ...inheritedShift });
2431
- continue;
2432
- }
2433
- warnUnreadPositions(sp, onWarn);
2434
- const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
2435
- if (tracker.startsLine(sp, afterText)) runLines.push([]);
2436
- push(readTspanRun(sp, gradients, leafStyle, onWarn));
2437
- }
2438
- }
2439
- if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2440
- const runs = joinLines(runLines);
2673
+ const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
2441
2674
  const plain = runs.map((r) => r.text).join("");
2442
- const pitched = tracker.lineHeight(fontSize);
2675
+ const pitched = laid ? numberAttr(el, LINE_HEIGHT_ATTR) : tracker.lineHeight(fontSize);
2443
2676
  if (pitched !== void 0) textStyle.lineHeight = pitched;
2444
2677
  const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
2445
2678
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
@@ -2450,7 +2683,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2450
2683
  const opacity = ownValue(el, "opacity");
2451
2684
  const node = {
2452
2685
  kind: "text",
2453
- x: ax - anchorOffset(textStyle, width2, plain),
2686
+ x: ax + lead - anchorOffset(textStyle, width2, plain),
2454
2687
  y: topY,
2455
2688
  width: width2,
2456
2689
  height: height2,
@@ -2476,6 +2709,38 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2476
2709
  placeTextNode(node, multiply(ctm, localTransform), onWarn);
2477
2710
  return node;
2478
2711
  }
2712
+ function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafStyle, onWarn) {
2713
+ const runLines = [[]];
2714
+ let leading = 0;
2715
+ let lead = 0;
2716
+ const push = (run) => {
2717
+ runLines[runLines.length - 1].push(run);
2718
+ };
2719
+ for (let i = 0; i < el.childNodes.length; i++) {
2720
+ const child = el.childNodes[i];
2721
+ if (child.nodeType === 3) {
2722
+ const t = child.textContent ?? "";
2723
+ if (!t) continue;
2724
+ push({ text: t, ...inheritedShift });
2725
+ } else if (child.nodeType === 1) {
2726
+ const sp = child;
2727
+ if (sp.tagName.toLowerCase() !== "tspan") {
2728
+ onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2729
+ const t = sp.textContent ?? "";
2730
+ if (t) push({ text: t, ...inheritedShift });
2731
+ continue;
2732
+ }
2733
+ warnUnreadPositions(sp, onWarn);
2734
+ const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
2735
+ const lines = tracker.startsLine(sp, afterText);
2736
+ if (runLines.length === 1 && runLines[0].length === 0 && statesLines(sp)) leading = lines;
2737
+ else for (let n = 0; n < lines; n++) runLines.push([]);
2738
+ lead += placeTspanRun(sp, readTspanRun(sp, gradients, leafStyle, onWarn), runLines, letterSpacing, onWarn);
2739
+ }
2740
+ }
2741
+ if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2742
+ return { runs: joinLines(runLines, leading), lead };
2743
+ }
2479
2744
  function readTspanRun(el, gradients, style, onWarn) {
2480
2745
  const text = el.textContent ?? "";
2481
2746
  const run = { text };
@@ -2922,10 +3187,10 @@ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix
2922
3187
  for (let i = 0; i < parent.children.length; i++) {
2923
3188
  const el = parent.children[i];
2924
3189
  const ns = el.namespaceURI;
2925
- if (ns && ns !== "http://www.w3.org/2000/svg") continue;
3190
+ if (ns && ns !== "http://www.w3.org/2000/svg" || isStrokeOutline(el)) continue;
2926
3191
  const hidden = isHiddenElement(el, style);
2927
3192
  if (hidden && !keepHidden) continue;
2928
- const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
3193
+ const node = unwrapStrokeGroup(el, parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden));
2929
3194
  if (!node) continue;
2930
3195
  const parsed = Array.isArray(node) ? node : [node];
2931
3196
  if (hidden) for (const n of parsed) n.hidden = true;
@@ -3018,7 +3283,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, kee
3018
3283
  }
3019
3284
  const leafStyle = deriveStyle(style, el);
3020
3285
  const fill = paintThroughMatrix(readPaint2(leafStyle, "fill", "#000000", gradients, onWarn), baked);
3021
- const ownStroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
3286
+ const ownStroke = withVariableWidth(el, readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el)));
3022
3287
  const stroke = ownStroke && strokeThroughMatrix(ownStroke, baked);
3023
3288
  if (stroke && typeof stroke.width === "number" && isNonUniform(baked)) {
3024
3289
  onWarn(`<${tag}> stroke sits under a skew or non-uniform scale; its width takes the uniform part only`);
@@ -3132,25 +3397,45 @@ function pathBounds(path) {
3132
3397
 
3133
3398
  // src/openSubpathFill.ts
3134
3399
  var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
3400
+ var marked = (stroke) => capsEnds(stroke) || stroke?.markerMid !== void 0;
3135
3401
  function withoutEndMarkers(stroke) {
3136
3402
  const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
3137
3403
  return rest;
3138
3404
  }
3139
- function withOpenSubpathsUnfilled(node) {
3140
- const path = node.path;
3141
- if (path.kind !== "polygon" || node.fill.kind === "none" || fillsOpenSubpaths(path)) return node;
3405
+ var closedStroke = (stroke) => stroke && capsEnds(stroke) ? withoutEndMarkers(stroke) : stroke;
3406
+ var isClosed = (part) => splitByClosure(part).open === null;
3407
+ function samePath(a, b) {
3408
+ if (a === b) return true;
3409
+ return a.commands.length === b.commands.length && a.coords.length === b.coords.length && a.commands.every((c, i) => c === b.commands[i]) && a.coords.every((v, i) => v === b.coords[i]);
3410
+ }
3411
+ function strokeParts(path, stroke, filled, closed, open) {
3412
+ const none = { kind: "none" };
3413
+ const subs = marked(stroke) ? splitSubpaths(path) : [];
3414
+ if (subs.length > 1) {
3415
+ return subs.map((sub) => ({ path: sub, fill: none, stroke: isClosed(sub) ? closedStroke(stroke) : stroke }));
3416
+ }
3417
+ if (closed && open && filled === closed) {
3418
+ return [{ path: closed, fill: none, stroke: closedStroke(stroke) }, { path: open, fill: none, stroke }];
3419
+ }
3420
+ return [{ path, fill: none, stroke: open ? stroke : closedStroke(stroke) }];
3421
+ }
3422
+ function pathAsSvgDraws(node) {
3423
+ const { path, fill, stroke } = node;
3424
+ if (path.kind !== "polygon") return capsEnds(stroke) ? { ...node, stroke: closedStroke(stroke) } : node;
3425
+ if (path.commands.length === 0) return node;
3142
3426
  const { closed, open } = splitByClosure(path);
3143
- if (!open) return node;
3144
- const { fill, stroke } = node;
3145
- const opens = capsEnds(stroke) ? splitSubpaths(open) : [open];
3146
- if (!closed && opens.length === 1) return { ...node, fill: { kind: "none" } };
3147
- const piece = (part, paint, line = stroke) => ({ kind: "path", path: part, fill: paint, ...line ? { stroke: line } : {} });
3427
+ const filled = fill.kind === "none" ? null : fillsOpenSubpaths(path) ? path : closed;
3428
+ const parts = strokeParts(path, stroke, filled, closed, open);
3429
+ const pieces = filled ? withFill(parts, filled, fill) : parts;
3430
+ if (pieces.length === 1) {
3431
+ const only = pieces[0];
3432
+ if (only.fill === fill && only.stroke === stroke) return node;
3433
+ const { stroke: _stroke, ...rest } = node;
3434
+ return { ...rest, fill: only.fill, ...only.stroke ? { stroke: only.stroke } : {} };
3435
+ }
3148
3436
  const group = {
3149
3437
  kind: "group",
3150
- children: [
3151
- ...closed ? [piece(closed, fill, stroke && withoutEndMarkers(stroke))] : [],
3152
- ...opens.map((part) => piece(part, { kind: "none" }))
3153
- ]
3438
+ children: pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
3154
3439
  };
3155
3440
  if (node.rotation != null && node.rotation !== 0) {
3156
3441
  const b = pathBounds(path);
@@ -3161,205 +3446,28 @@ function withOpenSubpathsUnfilled(node) {
3161
3446
  if (node.meta) group.meta = node.meta;
3162
3447
  return group;
3163
3448
  }
3164
-
3165
- // src/prettyXml.ts
3166
- var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
3167
- var isTag = (t) => t !== void 0 && t.startsWith("<");
3168
- var isClose = (t) => t.startsWith("</");
3169
- var isSelfClosing = (t) => t.endsWith("/>");
3170
- var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
3171
- function matchingClose(tokens, open) {
3172
- let depth = 0;
3173
- for (let i = open; i < tokens.length; i++) {
3174
- const t = tokens[i];
3175
- if (!isTag(t) || isSelfClosing(t)) continue;
3176
- depth += isClose(t) ? -1 : 1;
3177
- if (depth === 0) return i;
3178
- }
3179
- return tokens.length - 1;
3180
- }
3181
- function prettyXml(xml) {
3182
- const tokens = xml.match(TOKEN2) ?? [];
3183
- const lines = [];
3184
- let depth = 0;
3185
- for (let i = 0; i < tokens.length; i++) {
3186
- const t = tokens[i];
3187
- if (!isTag(t) || isSelfClosing(t)) {
3188
- lines.push(" ".repeat(depth) + t);
3189
- } else if (isClose(t)) {
3190
- depth--;
3191
- lines.push(" ".repeat(depth) + t);
3192
- } else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
3193
- const close2 = matchingClose(tokens, i);
3194
- lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
3195
- i = close2;
3196
- } else {
3197
- lines.push(" ".repeat(depth) + t);
3198
- depth++;
3199
- }
3200
- }
3201
- return lines.join("\n");
3202
- }
3203
-
3204
- // src/serialize.ts
3205
- function serializeSvg(nodes, opts = {}) {
3206
- const registry = new PaintServerRegistry();
3207
- registerPaintServers(nodes, registry);
3208
- const bounds = opts.viewBox ?? computeBounds(nodes);
3209
- const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3210
- const namespaces = opts.namespaces ?? {};
3211
- const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3212
- if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
3213
- rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3214
- }
3215
- for (const [prefix, uri] of Object.entries(namespaces)) {
3216
- rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
3217
- }
3218
- rootAttrs.push(`viewBox="${vb}"`);
3219
- if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
3220
- if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
3221
- if (opts.documentMeta) {
3222
- for (const prefix of Object.keys(namespaces)) {
3223
- const bucket = opts.documentMeta[prefix];
3224
- if (!bucket?.attrs) continue;
3225
- for (const [name, value] of Object.entries(bucket.attrs)) {
3226
- rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
3227
- }
3228
- }
3229
- }
3230
- const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
3231
- const defsXml = registry.toDefsXml(opts.onWarn);
3232
- const said = /* @__PURE__ */ new Set();
3233
- const warn = (m) => {
3234
- if (!opts.onWarn || said.has(m)) return;
3235
- said.add(m);
3236
- opts.onWarn(m);
3237
- };
3238
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
3239
- const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3240
- const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3241
- return opts.pretty ? prettyXml(xml) : xml;
3242
- }
3243
- function registerPaintServers(nodes, registry) {
3244
- for (const n of nodes) {
3245
- if (n.kind === "group") {
3246
- if (n.clip) registry.clipId(n.clip);
3247
- registerPaintServers(n.children, registry);
3248
- } else if (n.kind === "path") {
3249
- if (n.fill.kind === "gradient") registry.register(n.fill.paint);
3250
- if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
3251
- registerMarkers(n.stroke, registry);
3252
- } else if (n.kind === "text") {
3253
- registerTextPaint(n.fill?.paint, registry);
3254
- registerTextPaint(n.stroke?.paint, registry);
3255
- registerMarkers(n.stroke, registry);
3256
- for (const run of n.runs ?? []) {
3257
- registerTextPaint(run.fill?.paint, registry);
3258
- registerTextPaint(run.stroke?.paint, registry);
3259
- }
3260
- }
3261
- }
3262
- }
3263
- function registerTextPaint(paint, registry) {
3264
- if (paint && !isSolidPaint(paint)) registry.register(paint);
3265
- }
3266
- function registerMarkers(stroke, registry) {
3267
- if (!stroke) return;
3268
- for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
3269
- if (ref !== void 0) registry.markerId(ref);
3270
- }
3271
- }
3272
- function nodeXml(node, registry, namespaces, warn) {
3273
- const xml = kindXml(node, registry, namespaces, warn);
3274
- return node.hidden ? xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`) : xml;
3275
- }
3276
- function kindXml(node, registry, namespaces, warn) {
3277
- if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
3278
- if (node.kind === "text") return textXml(node, registry, namespaces, warn);
3279
- if (node.kind === "image") return imageXml(node, namespaces);
3280
- const drawn = withOpenSubpathsUnfilled(node);
3281
- return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
3449
+ function withFill(parts, filled, fill) {
3450
+ const host = parts.findIndex((p) => samePath(p.path, filled));
3451
+ if (host < 0) return [{ path: filled, fill, stroke: void 0 }, ...parts];
3452
+ return [{ ...parts[host], fill }, ...parts.filter((_, i) => i !== host)];
3282
3453
  }
3283
3454
  function warnStrokeAlign(align, warn) {
3284
3455
  if (align && align !== "center") {
3285
3456
  warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
3286
3457
  }
3287
3458
  }
3288
- function imageXml(node, namespaces) {
3289
- const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
3290
- const outer = [];
3291
- if (node.opacity != null && node.opacity !== 1) {
3292
- outer.push(`opacity="${trimNumber(node.opacity)}"`);
3293
- }
3294
- const frame = boxTransformAttr(node);
3295
- if (frame) outer.push(frame);
3296
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3297
- const metaEls = metaElementsXml(node.meta, namespaces);
3298
- const href = `href="${escapeAttr(node.href)}"`;
3299
- if (!node.source && !node.flipX && !node.flipY) {
3300
- const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
3301
- if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
3302
- return `<image ${attrs}${metaAttrs}/>`;
3303
- }
3304
- const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
3305
- const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
3306
- const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
3307
- const head = ['<g data-weasel-image="true"', ...outer].join(" ");
3308
- return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
3309
- }
3310
- function groupXml(node, registry, namespaces, warn) {
3311
- const attrs = [];
3312
- if (node.transform) {
3313
- const m = formatMatrix(node.transform);
3314
- if (m) attrs.push(`transform="${m}"`);
3315
- }
3316
- if (node.clip) {
3317
- attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
3318
- }
3319
- if (node.opacity != null && node.opacity !== 1) {
3320
- attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3321
- }
3322
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3323
- const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
3324
- const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
3325
- return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3326
- }
3327
- function pathXml(node, registry, namespaces, warn) {
3328
- const attrs = [`d="${serializePathD(node.path)}"`];
3329
- const fillAttrs = paintAttrs(node.fill, "fill", registry);
3330
- for (const a of fillAttrs) attrs.push(a);
3331
- if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
3332
- attrs.push(`fill-rule="evenodd"`);
3333
- }
3334
- if (node.stroke) {
3335
- const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
3336
- for (const a of strokeAttrs) attrs.push(a);
3337
- } else {
3338
- attrs.push('stroke="none"');
3339
- }
3340
- if (node.opacity != null && node.opacity !== 1) {
3341
- attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3342
- }
3343
- if (node.rotation != null && node.rotation !== 0) {
3344
- const b = pathBounds(node.path);
3345
- const cx = b.minX + (b.maxX - b.minX) / 2;
3346
- const cy = b.minY + (b.maxY - b.minY) / 2;
3347
- const deg = node.rotation * 180 / Math.PI;
3348
- attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
3349
- }
3350
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3351
- const metaEls = metaElementsXml(node.meta, namespaces);
3352
- if (metaEls) {
3353
- return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
3354
- }
3355
- return `<path ${attrs.join(" ")}${metaAttrs}/>`;
3356
- }
3357
3459
  function paintAttrs(paint, name, registry, includeOpacity = true) {
3358
3460
  if (paint.kind === "none") return [`${name}="none"`];
3359
3461
  const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
3360
3462
  if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
3361
3463
  return out;
3362
3464
  }
3465
+ function strokePaintAsFillAttrs(stroke, registry) {
3466
+ return [
3467
+ ...paintAttrs(stroke.paint, "fill", registry, false),
3468
+ ...opacityAttr("fill-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint))
3469
+ ];
3470
+ }
3363
3471
  function svgPaintOpacity(paint) {
3364
3472
  if (paint.kind === "solid") return paint.opacity;
3365
3473
  return paint.kind === "gradient" ? paint.paint.opacity : void 0;
@@ -3429,13 +3537,15 @@ function strokeAttrsFor(stroke, registry, warn) {
3429
3537
  }
3430
3538
  return attrs;
3431
3539
  }
3432
- function textXml(node, registry, namespaces, warn) {
3540
+ function textXml(node, registry, namespaces, warn, given) {
3541
+ const layout = given && layoutMeasured(given, node.text) ? given : null;
3542
+ const x = node.x + anchorOffset(node.style, node.width, node.text);
3433
3543
  const attrs = [
3434
- `x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
3544
+ `x="${trimNumber(x)}"`,
3435
3545
  `y="${trimNumber(node.y)}"`,
3436
- `dominant-baseline="text-before-edge"`,
3437
- // The runs model stores real line breaks; SVG's default whitespace
3438
- // handling would collapse them (and any indentation) away on re-import.
3546
+ layout ? `${LAID_ATTR}="lines"` : `dominant-baseline="text-before-edge"`,
3547
+ // SVG's default whitespace handling would collapse indentation and runs
3548
+ // of spaces away on re-import.
3439
3549
  `xml:space="preserve"`,
3440
3550
  `data-weasel-width="${trimNumber(node.width)}"`,
3441
3551
  `data-weasel-height="${trimNumber(node.height)}"`
@@ -3448,9 +3558,10 @@ function textXml(node, registry, namespaces, warn) {
3448
3558
  if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
3449
3559
  const direction = svgDirection(style, node.text);
3450
3560
  if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
3561
+ let anchor = "start";
3451
3562
  if (style?.align != null || direction === "rtl") {
3452
3563
  const align = style?.align ?? "left";
3453
- const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
3564
+ anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
3454
3565
  if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
3455
3566
  if (align === "justify") attrs.push('data-weasel-align="justify"');
3456
3567
  }
@@ -3465,13 +3576,14 @@ function textXml(node, registry, namespaces, warn) {
3465
3576
  }
3466
3577
  if (style?.wrap) {
3467
3578
  attrs.push('data-weasel-wrap="true"');
3468
- warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
3579
+ if (!layout) warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
3469
3580
  }
3470
3581
  if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
3471
3582
  if (node.verticalAlign && node.verticalAlign !== "top") {
3472
3583
  attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
3473
- warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
3584
+ if (!layout) warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
3474
3585
  }
3586
+ if (layout && style?.lineHeight != null) attrs.push(`${LINE_HEIGHT_ATTR}="${trimNumber(style.lineHeight)}"`);
3475
3587
  if (node.fill !== void 0) {
3476
3588
  const paint = node.fill?.paint;
3477
3589
  if (!paint) attrs.push('fill="none"');
@@ -3486,7 +3598,9 @@ function textXml(node, registry, namespaces, warn) {
3486
3598
  }
3487
3599
  const frame = boxTransformAttr(node);
3488
3600
  if (frame) attrs.push(frame);
3489
- const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
3601
+ const pieces = node.runs && node.runs.length > 0 ? node.runs.map((r) => ({ text: r.text, attrs: runAttrs(r, style, registry, warn) })) : [{ text: node.text, attrs: null }];
3602
+ const resolved = resolveTextStyle(style);
3603
+ const body = layout ? laidLinesXml(pieces, layout, { anchor, rtl: direction === "rtl" }) : linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
3490
3604
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3491
3605
  const metaEls = metaElementsXml(node.meta, namespaces);
3492
3606
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
@@ -3496,7 +3610,7 @@ function scriptSizeFace(run, style) {
3496
3610
  const face = resolveRunFace(run, resolveTextStyle(style));
3497
3611
  return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3498
3612
  }
3499
- function runXml(run, style, registry, warn) {
3613
+ function runAttrs(run, style, registry, warn) {
3500
3614
  const attrs = [];
3501
3615
  if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
3502
3616
  else if (run.bold) attrs.push(`font-weight="700"`);
@@ -3526,8 +3640,7 @@ function runXml(run, style, registry, warn) {
3526
3640
  }
3527
3641
  for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
3528
3642
  if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
3529
- const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
3530
- return `${head}${escapeText(run.text)}</tspan>`;
3643
+ return attrs;
3531
3644
  }
3532
3645
  function textDecorationValue(underline, strikethrough, overline) {
3533
3646
  const tokens = [];
@@ -3536,17 +3649,212 @@ function textDecorationValue(underline, strikethrough, overline) {
3536
3649
  if (overline) tokens.push("overline");
3537
3650
  return tokens.length > 0 ? tokens.join(" ") : null;
3538
3651
  }
3539
- function svgNeeds(nodes) {
3652
+
3653
+ // src/prettyXml.ts
3654
+ var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
3655
+ var isTag = (t) => t !== void 0 && t.startsWith("<");
3656
+ var isClose = (t) => t.startsWith("</");
3657
+ var isSelfClosing = (t) => t.endsWith("/>");
3658
+ var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
3659
+ function matchingClose(tokens, open) {
3660
+ let depth = 0;
3661
+ for (let i = open; i < tokens.length; i++) {
3662
+ const t = tokens[i];
3663
+ if (!isTag(t) || isSelfClosing(t)) continue;
3664
+ depth += isClose(t) ? -1 : 1;
3665
+ if (depth === 0) return i;
3666
+ }
3667
+ return tokens.length - 1;
3668
+ }
3669
+ function prettyXml(xml) {
3670
+ const tokens = xml.match(TOKEN2) ?? [];
3671
+ const lines = [];
3672
+ let depth = 0;
3673
+ for (let i = 0; i < tokens.length; i++) {
3674
+ const t = tokens[i];
3675
+ if (!isTag(t) || isSelfClosing(t)) {
3676
+ lines.push(" ".repeat(depth) + t);
3677
+ } else if (isClose(t)) {
3678
+ depth--;
3679
+ lines.push(" ".repeat(depth) + t);
3680
+ } else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
3681
+ const close2 = matchingClose(tokens, i);
3682
+ lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
3683
+ i = close2;
3684
+ } else {
3685
+ lines.push(" ".repeat(depth) + t);
3686
+ depth++;
3687
+ }
3688
+ }
3689
+ return lines.join("\n");
3690
+ }
3691
+
3692
+ // src/serialize.ts
3693
+ function serializeSvg(nodes, opts = {}) {
3694
+ const registry = new PaintServerRegistry();
3695
+ registerPaintServers(nodes, registry);
3696
+ const bounds = opts.viewBox ?? computeBounds(nodes);
3697
+ const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3698
+ const namespaces = opts.namespaces ?? {};
3699
+ const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3700
+ if ((registry.usesPrivateNamespace() || writesVariableWidth(nodes)) && namespaces[WEASEL_NS_PREFIX] == null) {
3701
+ rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3702
+ }
3703
+ for (const [prefix, uri] of Object.entries(namespaces)) {
3704
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
3705
+ }
3706
+ rootAttrs.push(`viewBox="${vb}"`);
3707
+ if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
3708
+ if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
3709
+ if (opts.documentMeta) {
3710
+ for (const prefix of Object.keys(namespaces)) {
3711
+ const bucket = opts.documentMeta[prefix];
3712
+ if (!bucket?.attrs) continue;
3713
+ for (const [name, value] of Object.entries(bucket.attrs)) {
3714
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
3715
+ }
3716
+ }
3717
+ }
3718
+ const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
3719
+ const defsXml = registry.toDefsXml(opts.onWarn);
3720
+ const said = /* @__PURE__ */ new Set();
3721
+ const warn = (m) => {
3722
+ if (!opts.onWarn || said.has(m)) return;
3723
+ said.add(m);
3724
+ opts.onWarn(m);
3725
+ };
3726
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn, opts.textLayout)).join("");
3727
+ const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3728
+ const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3729
+ return opts.pretty ? prettyXml(xml) : xml;
3730
+ }
3731
+ function registerPaintServers(nodes, registry) {
3732
+ for (const n of nodes) {
3733
+ if (n.kind === "group") {
3734
+ if (n.clip) registry.clipId(n.clip);
3735
+ registerPaintServers(n.children, registry);
3736
+ } else if (n.kind === "path") {
3737
+ if (n.fill.kind === "gradient") registry.register(n.fill.paint);
3738
+ if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
3739
+ registerMarkers(n.stroke, registry);
3740
+ } else if (n.kind === "text") {
3741
+ registerTextPaint(n.fill?.paint, registry);
3742
+ registerTextPaint(n.stroke?.paint, registry);
3743
+ registerMarkers(n.stroke, registry);
3744
+ for (const run of n.runs ?? []) {
3745
+ registerTextPaint(run.fill?.paint, registry);
3746
+ registerTextPaint(run.stroke?.paint, registry);
3747
+ }
3748
+ }
3749
+ }
3750
+ }
3751
+ function registerTextPaint(paint, registry) {
3752
+ if (paint && !isSolidPaint(paint)) registry.register(paint);
3753
+ }
3754
+ function registerMarkers(stroke, registry) {
3755
+ if (!stroke) return;
3756
+ for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
3757
+ if (ref !== void 0) registry.markerId(ref);
3758
+ }
3759
+ }
3760
+ function nodeXml(node, registry, namespaces, warn, layoutOf) {
3761
+ const xml = kindXml(node, registry, namespaces, warn, layoutOf);
3762
+ if (!node.hidden) return xml;
3763
+ return xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`).replace(/<path (?=[^>]*:role="stroke-outline")/, '<path display="none" ');
3764
+ }
3765
+ function kindXml(node, registry, namespaces, warn, layoutOf) {
3766
+ if (node.kind === "group") return groupXml(node, registry, namespaces, warn, layoutOf);
3767
+ if (node.kind === "text") return textXml(node, registry, namespaces, warn, layoutOf?.(node));
3768
+ if (node.kind === "image") return imageXml(node, namespaces);
3769
+ const drawn = pathAsSvgDraws(node);
3770
+ return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn, layoutOf) : pathXml(drawn, registry, namespaces, warn);
3771
+ }
3772
+ function imageXml(node, namespaces) {
3773
+ const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
3774
+ const outer = [];
3775
+ if (node.opacity != null && node.opacity !== 1) {
3776
+ outer.push(`opacity="${trimNumber(node.opacity)}"`);
3777
+ }
3778
+ const frame = boxTransformAttr(node);
3779
+ if (frame) outer.push(frame);
3780
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3781
+ const metaEls = metaElementsXml(node.meta, namespaces);
3782
+ const href = `href="${escapeAttr(node.href)}"`;
3783
+ if (!node.source && !node.flipX && !node.flipY) {
3784
+ const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
3785
+ if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
3786
+ return `<image ${attrs}${metaAttrs}/>`;
3787
+ }
3788
+ const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
3789
+ const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
3790
+ const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
3791
+ const head = ['<g data-weasel-image="true"', ...outer].join(" ");
3792
+ return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
3793
+ }
3794
+ function groupXml(node, registry, namespaces, warn, layoutOf) {
3795
+ const attrs = [];
3796
+ if (node.transform) {
3797
+ const m = formatMatrix(node.transform);
3798
+ if (m) attrs.push(`transform="${m}"`);
3799
+ }
3800
+ if (node.clip) {
3801
+ attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
3802
+ }
3803
+ if (node.opacity != null && node.opacity !== 1) {
3804
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3805
+ }
3806
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3807
+ const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
3808
+ const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn, layoutOf)).join("");
3809
+ return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3810
+ }
3811
+ function pathXml(node, registry, namespaces, warn) {
3812
+ const attrs = [`d="${serializePathD(node.path)}"`];
3813
+ const fillAttrs = paintAttrs(node.fill, "fill", registry);
3814
+ for (const a of fillAttrs) attrs.push(a);
3815
+ if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
3816
+ attrs.push(`fill-rule="evenodd"`);
3817
+ }
3818
+ const varies = hasVariableWidth(node.stroke);
3819
+ if (node.stroke) {
3820
+ const strokeAttrs = strokeAttrsFor(node.stroke, registry, varies ? () => {
3821
+ } : warn);
3822
+ for (const a of varies ? variableWidthAttrs(node.stroke, strokeAttrs) : strokeAttrs) attrs.push(a);
3823
+ } else {
3824
+ attrs.push('stroke="none"');
3825
+ }
3826
+ const opacity = node.opacity != null && node.opacity !== 1 ? `opacity="${trimNumber(node.opacity)}"` : null;
3827
+ if (opacity && !varies) attrs.push(opacity);
3828
+ const transform = rotationAttr(node);
3829
+ if (transform) attrs.push(transform);
3830
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3831
+ const metaEls = metaElementsXml(node.meta, namespaces);
3832
+ const xml = metaEls ? `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>` : `<path ${attrs.join(" ")}${metaAttrs}/>`;
3833
+ if (!varies) return xml;
3834
+ const both = xml + strokeOutlineXml(node.path, node.stroke, strokePaintAsFillAttrs(node.stroke, registry), transform);
3835
+ return opacity ? strokeGroupXml(opacity, both) : both;
3836
+ }
3837
+ function rotationAttr(node) {
3838
+ if (node.rotation == null || node.rotation === 0) return null;
3839
+ const b = pathBounds(node.path);
3840
+ const cx = b.minX + (b.maxX - b.minX) / 2;
3841
+ const cy = b.minY + (b.maxY - b.minY) / 2;
3842
+ const deg = node.rotation * 180 / Math.PI;
3843
+ return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
3844
+ }
3845
+ function svgNeeds(nodes, opts = {}) {
3540
3846
  const registry = new PaintServerRegistry();
3541
3847
  registerPaintServers(nodes, registry);
3542
3848
  const fonts = /* @__PURE__ */ new Map();
3849
+ const add = (face) => fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3543
3850
  const walk = (list) => {
3544
3851
  for (const n of list) {
3545
3852
  if (n.kind === "group") walk(n.children);
3546
3853
  else if (n.kind === "text") {
3854
+ if (opts.textLayout) layoutFaces(n).forEach(add);
3547
3855
  for (const run of n.runs ?? []) {
3548
3856
  const face = scriptSizeFace(run, n.style);
3549
- if (face) fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3857
+ if (face) add(face);
3550
3858
  }
3551
3859
  }
3552
3860
  }
@@ -3554,8 +3862,15 @@ function svgNeeds(nodes) {
3554
3862
  walk(nodes);
3555
3863
  return { fonts: [...fonts.values()], paintKinds: registry.paintKinds() };
3556
3864
  }
3557
- function warmSvg(nodes) {
3558
- const needs = svgNeeds(nodes);
3865
+ function layoutFaces(node) {
3866
+ const style = resolveTextStyle(node.style);
3867
+ return toRuns(node.runs && node.runs.length > 0 ? node.runs : node.text).map((run) => {
3868
+ const face = resolveRunFace(run, style);
3869
+ return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3870
+ });
3871
+ }
3872
+ function warmSvg(nodes, opts = {}) {
3873
+ const needs = svgNeeds(nodes, opts);
3559
3874
  return Promise.all([
3560
3875
  warmFonts(needs.fonts),
3561
3876
  warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
@@ -3608,7 +3923,9 @@ function strokeDataFromSvg(stroke, box) {
3608
3923
  ...stroke.miterLimit !== void 0 ? { miterLimit: stroke.miterLimit } : {},
3609
3924
  ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
3610
3925
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
3611
- ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
3926
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
3927
+ ...stroke.vertexWidths !== void 0 ? { vertexWidths: stroke.vertexWidths } : {},
3928
+ ...stroke.nib !== void 0 ? { nib: stroke.nib } : {}
3612
3929
  };
3613
3930
  }
3614
3931
  function fillInBoxFrame(fill, box) {
@@ -3651,9 +3968,12 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3651
3968
  ...n.hidden ? { hidden: true } : {}
3652
3969
  };
3653
3970
  drafts.push(draft);
3971
+ const at = drafts.length - 1;
3654
3972
  const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
3655
3973
  if (!acc) {
3656
- drafts.splice(drafts.indexOf(draft), 1);
3974
+ const kept = drafts.length > at + 1 || (options.keepEmpty?.(n) ?? false);
3975
+ if (!kept) drafts.splice(at, 1);
3976
+ else if (n.clip) draft.pose = boxOfPath(n.clip);
3657
3977
  return null;
3658
3978
  }
3659
3979
  draft.pose = n.clip ? boxOfPath(n.clip) : acc;
@@ -3737,7 +4057,7 @@ async function unpackSvgFiles(files, ctx) {
3737
4057
  for (const file of files) {
3738
4058
  try {
3739
4059
  const text = await readFileText(file);
3740
- const parsed = parseSvg(text);
4060
+ const parsed = parseSvg(text, { keepHidden: true });
3741
4061
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
3742
4062
  let drafts = svgNodesToKitDrafts(parsed, () => ctx.scene.mintId());
3743
4063
  if (drafts.length === 0) {
@@ -3782,7 +4102,8 @@ async function unpackSvgFiles(files, ctx) {
3782
4102
  layer,
3783
4103
  pose: place(d.pose),
3784
4104
  data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
3785
- parent: d.parentId
4105
+ parent: d.parentId,
4106
+ ...d.hidden ? { hidden: true } : {}
3786
4107
  },
3787
4108
  label: "Insert SVG"
3788
4109
  })
@@ -3817,7 +4138,9 @@ function svgStrokeFromKit(stroke, box) {
3817
4138
  ...stroke.align !== void 0 ? { align: stroke.align } : {},
3818
4139
  ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
3819
4140
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
3820
- ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
4141
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
4142
+ ...stroke.vertexWidths?.length ? { vertexWidths: stroke.vertexWidths } : {},
4143
+ ...stroke.nib ? { nib: stroke.nib } : {}
3821
4144
  };
3822
4145
  }
3823
4146
  function svgImageFromKit(image, pose) {
@@ -3851,22 +4174,12 @@ function runInPoseFrame(run, box) {
3851
4174
  function svgLeafFromKit(data, pose) {
3852
4175
  if (data.image) return svgImageFromKit(data.image, pose);
3853
4176
  if (data.text != null) {
3854
- const node2 = {
3855
- kind: "text",
3856
- x: pose.x,
3857
- y: pose.y,
3858
- width: pose.width,
3859
- height: pose.height,
3860
- text: data.text
3861
- };
3862
- if (data.style) node2.style = data.style;
3863
- if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
3864
- if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
3865
- if (data.fill !== void 0) node2.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
3866
- if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
3867
- if (pose.rotation) node2.rotation = pose.rotation;
3868
- if (pose.transform) node2.transform = pose.transform;
3869
- return node2;
4177
+ const text = svgTextFromKit({ ...data, text: data.text }, pose);
4178
+ const boxPath = { kind: "rect", x: pose.x, y: pose.y, width: pose.width, height: pose.height };
4179
+ const clip = data.overflow === "clip" ? { clip: pathThroughPoseFrame(boxPath, pose) } : {};
4180
+ if (!data.background?.paint) return clip.clip ? { kind: "group", children: [text], ...clip } : text;
4181
+ const box = svgLeafFromKit({ path: boxPath, fill: data.background }, pose);
4182
+ return { kind: "group", children: [box, text], ...clip };
3870
4183
  }
3871
4184
  if (!data.path) return null;
3872
4185
  const stroke = svgStrokeFromKit(data.stroke, pose);
@@ -3885,21 +4198,53 @@ function svgLeafFromKit(data, pose) {
3885
4198
  } else if (pose.rotation) node.rotation = pose.rotation;
3886
4199
  return node;
3887
4200
  }
4201
+ function svgTextFromKit(data, pose) {
4202
+ const pad = Math.max(0, Math.min(data.padding ?? 0, pose.width / 2, pose.height / 2));
4203
+ const node = {
4204
+ kind: "text",
4205
+ x: pose.x + pad,
4206
+ y: pose.y + pad,
4207
+ width: pose.width - 2 * pad,
4208
+ height: pose.height - 2 * pad,
4209
+ text: data.text
4210
+ };
4211
+ if (data.style) node.style = data.style;
4212
+ if (data.runs) node.runs = data.runs.map((r) => runInPoseFrame(r, pose));
4213
+ if (data.verticalAlign) node.verticalAlign = data.verticalAlign;
4214
+ if (data.fill !== void 0) node.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
4215
+ if (data.stroke) node.stroke = strokeInPoseFrame(data.stroke, pose);
4216
+ if (pose.rotation) node.rotation = pose.rotation;
4217
+ if (pose.transform) node.transform = pose.transform;
4218
+ return node;
4219
+ }
3888
4220
  function svgNodesFromKit(tree, options = {}) {
3889
4221
  const { include, leaf, group } = options;
3890
4222
  const emit = (id) => {
3891
- if (include && !include(id)) return null;
3892
4223
  const node = tree.get(id);
3893
- if (!node) return null;
4224
+ const out2 = node && (!include || include(id)) ? lower(id, node) : null;
4225
+ return out2 && node?.hidden === true ? { ...out2, hidden: true } : out2;
4226
+ };
4227
+ const lower = (id, node) => {
3894
4228
  if (node.kind === "container") {
3895
- const children = [];
3896
- for (const c of tree.childrenOf(id)) {
3897
- const child = emit(c);
3898
- if (child) children.push(child);
4229
+ const emitChildren = () => {
4230
+ const children = [];
4231
+ for (const c of tree.childrenOf(id)) {
4232
+ const child = emit(c);
4233
+ if (child) children.push(child);
4234
+ }
4235
+ return children;
4236
+ };
4237
+ const path = node.data?.path;
4238
+ const clip = path ? pathThroughPoseFrame(pathInPoseFrame(path, node.pose), node.pose) : void 0;
4239
+ const copies = node.copiesFromPose?.(node.pose, node) ?? [];
4240
+ const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
4241
+ let g = content();
4242
+ if (copies.length > 0) {
4243
+ g = {
4244
+ kind: "group",
4245
+ children: [g, ...copies.map((transform) => ({ kind: "group", transform, children: [content()] }))]
4246
+ };
3899
4247
  }
3900
- const g = { kind: "group", children };
3901
- const clip = node.data?.path;
3902
- if (clip) g.clip = pathThroughPoseFrame(pathInPoseFrame(clip, node.pose), node.pose);
3903
4248
  return group ? group(id, g) : g;
3904
4249
  }
3905
4250
  return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);
@@ -3912,6 +4257,6 @@ function svgNodesFromKit(tree, options = {}) {
3912
4257
  return out;
3913
4258
  }
3914
4259
 
3915
- export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
4260
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializePathD, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
3916
4261
  //# sourceMappingURL=index.js.map
3917
4262
  //# sourceMappingURL=index.js.map