@weasel-js/svg 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -183,6 +183,14 @@ writes a `kit:image` leaf back as an `SvgImageNode`.
183
183
  in (a non-`none` value warns) and written back as `none`, so a source file
184
184
  that relied on letterboxing imports stretched.
185
185
 
186
+ ## Writing XML by hand
187
+
188
+ `escapeAttr(s)` and `escapeText(s)` are the escaping this package writes XML
189
+ with, for a caller writing its own. Both drop the characters XML 1.0 cannot
190
+ hold, even as references, and turn a vertical tab or form feed into a line
191
+ break. `escapeAttr` also writes tabs and line breaks as references, since a
192
+ parser reads a literal one in an attribute as a space.
193
+
186
194
  ## License
187
195
 
188
196
  MIT
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Affine } from '@weasel-js/geom';
2
+ export { pathToD as serializePathD } from '@weasel-js/geom';
2
3
  import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, VertexWidth, StrokeNib, StyledRun, TextStyle, TextVerticalAlign, Fill, Stroke, PoseLinear, MarkerEntry, TextPoseLayout, RenderNeeds, TilePatternSpec, ImageNodeData, IngestCtx } from '@weasel-js/core';
3
4
 
4
5
  /**
@@ -441,18 +442,19 @@ declare function nativeSvgSpace(space: string | null | undefined): boolean;
441
442
  declare function serializeSvg(nodes: SvgNode[], opts?: SerializeOptions): string;
442
443
 
443
444
  /**
444
- * Serialize a weasel `Path` to an SVG `d=` attribute string. `RectPath`
445
- * lowers to `M h v h Z`; `PolygonPath` walks the (commands, coords)
446
- * arrays, emitting one SVG command per kit command, with corner shapes
447
- * expanded into the geometry they draw.
445
+ * Escaping for the XML this package writes by hand.
448
446
  *
449
- * Output uses absolute commands only and a small amount of whitespace
450
- * compression (no leading zeros stripped beyond what `Number.toString`
451
- * already does). Numbers are rounded to ~6 decimals via `trimNumber`.
447
+ * XML 1.0 cannot hold most C0 controls, U+FFFE, U+FFFF, or half a surrogate
448
+ * pair, even as character references, and a parser rejects the whole
449
+ * document over one. A vertical tab or form feed is a line break where text
450
+ * comes from (a word processor's clipboard), so those become `\n`; the rest
451
+ * are dropped.
452
452
  */
453
-
454
- /** Serialize a weasel `Path` to an SVG `d=` attribute string. */
455
- declare function serializePathD(given: Path): string;
453
+ /** A parser turns a literal tab or line break in an attribute value into a
454
+ * space, so those are written as character references. */
455
+ declare function escapeAttr(s: string): string;
456
+ /** A literal carriage return in content is read back as a newline. */
457
+ declare function escapeText(s: string): string;
456
458
 
457
459
  /**
458
460
  * What `serializeSvg(nodes)` reads that loads on demand: the paint kind of
@@ -701,4 +703,4 @@ declare function svgNodesToKitDrafts<TData = Record<string, unknown>>(input: Par
701
703
  */
702
704
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
703
705
 
704
- export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SupportsOptions, type SvgDraftBounds, type SvgDraftPose, type SvgGroupNode, type SvgImageNode, type SvgKitLeafData, type SvgKitPose, type SvgKitTree, type SvgKitTreeNode, type SvgLeafNode, type SvgMediaEnvironment, type SvgNode, type SvgNodesFromKitOptions, type SvgNodesToKitDraftsOptions, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializePathD, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
706
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SupportsOptions, type SvgDraftBounds, type SvgDraftPose, type SvgGroupNode, type SvgImageNode, type SvgKitLeafData, type SvgKitPose, type SvgKitTree, type SvgKitTreeNode, type SvgLeafNode, type SvgMediaEnvironment, type SvgNode, type SvgNodesFromKitOptions, type SvgNodesToKitDraftsOptions, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, escapeAttr, escapeText, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
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';
1
+ import { identity, multiply, pathToD, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
2
+ export { pathToD as serializePathD } from '@weasel-js/geom';
3
+ import { 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, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
4
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
5
 
5
6
  // src/types.ts
@@ -1383,40 +1384,6 @@ function readPattern(el, onWarn) {
1383
1384
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1384
1385
  return paint;
1385
1386
  }
1386
- function serializePathD(given) {
1387
- const path = resolveCorners(given);
1388
- if (path.kind === "rect") {
1389
- const { x, y, width: width2, height: height2 } = path;
1390
- return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width2)}v${trimNumber(height2)}h${trimNumber(-width2)}Z`;
1391
- }
1392
- const cmds = path.commands;
1393
- const xs = path.coords;
1394
- const out = [];
1395
- let ci = 0;
1396
- for (let i = 0; i < cmds.length; i++) {
1397
- const c = cmds[i];
1398
- if (c === PATH_M) {
1399
- out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
1400
- ci += 2;
1401
- } else if (c === PATH_L) {
1402
- out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
1403
- ci += 2;
1404
- } else if (c === PATH_C) {
1405
- out.push(
1406
- `C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
1407
- );
1408
- ci += 6;
1409
- } else if (c === PATH_Q) {
1410
- out.push(
1411
- `Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
1412
- );
1413
- ci += 4;
1414
- } else if (c === PATH_Z) {
1415
- out.push("Z");
1416
- }
1417
- }
1418
- return out.join("");
1419
- }
1420
1387
 
1421
1388
  // src/gradients.ts
1422
1389
  var WEASEL_NS = "urn:weasel-js:svg";
@@ -1722,7 +1689,7 @@ var PaintServerRegistry = class {
1722
1689
  }
1723
1690
  for (const path of this.clips) {
1724
1691
  parts.push(
1725
- `<clipPath id="${this.clipIds.get(path)}"><path d="${serializePathD(path)}"/></clipPath>`
1692
+ `<clipPath id="${this.clipIds.get(path)}"><path d="${pathToD(path)}"/></clipPath>`
1726
1693
  );
1727
1694
  }
1728
1695
  parts.push("</defs>");
@@ -1736,7 +1703,7 @@ function defaultMarkerXml(id, entry, onWarn, key, size) {
1736
1703
  const name = key ?? id;
1737
1704
  const unit = size === void 0 ? 1 : resolveMarkerSize({ key: name, size }, 1);
1738
1705
  const path = entry.path({ size: unit, stroke: { paint: { kind: "solid", color: "#000" } } });
1739
- const d = serializePathD(path);
1706
+ const d = pathToD(path);
1740
1707
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
1741
1708
  const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width * unit)}" stroke-linecap="round" stroke-linejoin="round"` : "";
1742
1709
  const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
@@ -2159,7 +2126,7 @@ function strokeOutlineXml(path, stroke, fillAttrs, transform) {
2159
2126
  ...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {}
2160
2127
  };
2161
2128
  const outline = strokeToPath(path, kit);
2162
- const attrs = [`d="${serializePathD(outline)}"`, ...fillAttrs, 'stroke="none"'];
2129
+ const attrs = [`d="${pathToD(outline)}"`, ...fillAttrs, 'stroke="none"'];
2163
2130
  if (transform) attrs.push(transform);
2164
2131
  attrs.push(`${WEASEL_NS_PREFIX}:${ROLE}="${OUTLINE}"`);
2165
2132
  return `<path ${attrs.join(" ")}/>`;
@@ -2403,13 +2370,17 @@ var LineTracker = class {
2403
2370
  return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2404
2371
  }
2405
2372
  };
2373
+ var holdsOnlyText = (el) => !Array.from(el.childNodes).some(
2374
+ (child) => child.nodeType === 1
2375
+ /* ELEMENT_NODE */
2376
+ );
2406
2377
  function warnUnreadPositions(el, onWarn) {
2407
2378
  const tag = el.tagName.toLowerCase();
2408
2379
  for (const name of ["x", "y"]) {
2409
2380
  const raw = el.getAttribute(name)?.trim() ?? "";
2410
2381
  if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2411
2382
  }
2412
- for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
2383
+ for (const name of tag === "tspan" || holdsOnlyText(el) ? ["rotate"] : ["dx", "rotate"]) {
2413
2384
  const raw = el.getAttribute(name)?.trim() ?? "";
2414
2385
  if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2415
2386
  onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
@@ -2540,8 +2511,9 @@ function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
2540
2511
  return 0;
2541
2512
  }
2542
2513
  const raw = tspan.getAttribute("dx");
2514
+ const tag = tspan.tagName.toLowerCase();
2543
2515
  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`);
2516
+ onWarn(`<${tag} dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
2545
2517
  line.push(run);
2546
2518
  return 0;
2547
2519
  }
@@ -2554,7 +2526,7 @@ function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
2554
2526
  } else if (dx[0] !== 0 && lines.length === 1) {
2555
2527
  lead = dx[0];
2556
2528
  } 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`);
2529
+ onWarn(`<${tag} dx="${raw}"> opens a line; the line starts at the box edge, and the rest of the list is read`);
2558
2530
  }
2559
2531
  line.push(...spread(run, dx.slice(1), inherited2));
2560
2532
  return lead;
@@ -2716,7 +2688,10 @@ function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafSty
2716
2688
  const push = (run) => {
2717
2689
  runLines[runLines.length - 1].push(run);
2718
2690
  };
2719
- for (let i = 0; i < el.childNodes.length; i++) {
2691
+ if (holdsOnlyText(el) && el.hasAttribute("dx")) {
2692
+ const t = el.textContent ?? "";
2693
+ if (t) lead = placeTspanRun(el, { text: t, ...inheritedShift }, runLines, letterSpacing, onWarn);
2694
+ } else for (let i = 0; i < el.childNodes.length; i++) {
2720
2695
  const child = el.childNodes[i];
2721
2696
  if (child.nodeType === 3) {
2722
2697
  const t = child.textContent ?? "";
@@ -3809,7 +3784,7 @@ function groupXml(node, registry, namespaces, warn, layoutOf) {
3809
3784
  return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3810
3785
  }
3811
3786
  function pathXml(node, registry, namespaces, warn) {
3812
- const attrs = [`d="${serializePathD(node.path)}"`];
3787
+ const attrs = [`d="${pathToD(node.path)}"`];
3813
3788
  const fillAttrs = paintAttrs(node.fill, "fill", registry);
3814
3789
  for (const a of fillAttrs) attrs.push(a);
3815
3790
  if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
@@ -4257,6 +4232,6 @@ function svgNodesFromKit(tree, options = {}) {
4257
4232
  return out;
4258
4233
  }
4259
4234
 
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 };
4235
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, escapeAttr, escapeText, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
4261
4236
  //# sourceMappingURL=index.js.map
4262
4237
  //# sourceMappingURL=index.js.map