@weasel-js/svg 1.10.1 → 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
@@ -37,6 +37,16 @@ answer, and the alternative is unrepresentable anyway (see above). It only
37
37
  shows up on foreign SVG that sets decoration on a group and cancels it on a
38
38
  child.
39
39
 
40
+ **SVG text does not wrap.** By default a `<text>` is written one line per
41
+ hard break, pitched at the node's font size times its line height, and a
42
+ wrapping or justified node keeps that only in `data-weasel-*` attributes for
43
+ weasel's own reader. Pass `serializeSvg(nodes, { textLayout: layoutTextPose })`
44
+ to write the lines the renderer draws instead: each line is a `<tspan>` at the
45
+ layout's `x` and baseline `y`, each word of a line the layout justified gets
46
+ its own `x`, and a line holding a larger run sits where the renderer puts it.
47
+ The layout measures with the registered fonts, so `await warmSvg(nodes, {
48
+ textLayout })` first; a node no face could measure is written the default way.
49
+
40
50
  ## Paints SVG cannot express
41
51
 
42
52
  SVG has `<linearGradient>`, `<radialGradient>` and `<pattern>` and nothing
@@ -173,6 +183,14 @@ writes a `kit:image` leaf back as an `SvgImageNode`.
173
183
  in (a non-`none` value warns) and written back as `none`, so a source file
174
184
  that relied on letterboxing imports stretched.
175
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
+
176
194
  ## License
177
195
 
178
196
  MIT
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Affine } from '@weasel-js/geom';
2
- import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, StyledRun, TextStyle, TextVerticalAlign, Fill, Stroke, PoseLinear, MarkerEntry, RenderNeeds, TilePatternSpec, ImageNodeData, IngestCtx } from '@weasel-js/core';
2
+ export { pathToD as serializePathD } from '@weasel-js/geom';
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
  /**
5
6
  * Media Queries 4 evaluation against a fixed environment.
@@ -125,6 +126,13 @@ interface SvgStroke {
125
126
  markerStart?: MarkerRef;
126
127
  markerMid?: MarkerRef;
127
128
  markerEnd?: MarkerRef;
129
+ /** The kit stroke's `vertexWidths` and `nib`. SVG has no variable width, so
130
+ * the serializer writes such a stroke's outline as a filled shape after the
131
+ * path, and keeps these in the private namespace for the parser. An entry
132
+ * of `vertexWidths`, or one side of a pair, that is not a number of zero or
133
+ * more takes `width`. */
134
+ vertexWidths?: VertexWidth[];
135
+ nib?: StrokeNib;
128
136
  }
129
137
  /**
130
138
  * Leaf node: a path geometry plus fill/stroke. Other shapes (`<rect>`,
@@ -372,6 +380,18 @@ interface SerializeOptions {
372
380
  * line, since whitespace inside it is content.
373
381
  */
374
382
  pretty?: boolean;
383
+ /**
384
+ * The lines a text node lays out into — pass core's `layoutTextPose`, the
385
+ * layout the renderer draws with. With it, each line is written as a
386
+ * `<tspan>` at the layout's own `x` and baseline `y`, a justified line
387
+ * gives each word its own `x`, and a line holding a larger run is pitched
388
+ * as the renderer pitches it, so a reader that cannot wrap draws the lines
389
+ * where weasel does. A node it returns nothing for, or lays out to zero
390
+ * width because no face could measure it, is written without it: one line
391
+ * per hard break, at the node's own pitch. Fonts it reads must have loaded;
392
+ * `warmSvg(nodes, { textLayout })` loads them.
393
+ */
394
+ textLayout?: (node: SvgTextNode) => TextPoseLayout | null | undefined;
375
395
  }
376
396
 
377
397
  /**
@@ -421,22 +441,38 @@ declare function nativeSvgSpace(space: string | null | undefined): boolean;
421
441
  */
422
442
  declare function serializeSvg(nodes: SvgNode[], opts?: SerializeOptions): string;
423
443
 
444
+ /**
445
+ * Escaping for the XML this package writes by hand.
446
+ *
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
+ */
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;
458
+
424
459
  /**
425
460
  * What `serializeSvg(nodes)` reads that loads on demand: the paint kind of
426
461
  * every fill and stroke it writes as a paint server, and the face of every
427
462
  * run whose script size it takes from font metrics. The kinds come from the
428
463
  * serializer's own paint pre-pass, so the two cannot disagree. Registered
429
- * kinds are included.
464
+ * kinds are included. Given the `textLayout` the export will run, every face
465
+ * a text node is set in, which that layout measures with.
430
466
  */
431
- declare function svgNeeds(nodes: readonly SvgNode[]): RenderNeeds;
467
+ declare function svgNeeds(nodes: readonly SvgNode[], opts?: Pick<SerializeOptions, 'textLayout'>): RenderNeeds;
432
468
  /**
433
- * Load what {@link svgNeeds} lists, so a `serializeSvg(nodes)` issued after it
434
- * resolves writes every paint server's def and every script size from the
435
- * face it renders in. Only those load: a lazily registered kind or font the
469
+ * Load what {@link svgNeeds} lists, so a `serializeSvg(nodes, opts)` issued
470
+ * after it resolves writes every paint server's def, and every script size and
471
+ * laid-out line from the face it renders in. Only those load: a lazily registered kind or font the
436
472
  * nodes do not use is never fetched, and cannot fail the export. A kind
437
473
  * nothing registered is passed over; the export warns about it instead.
438
474
  */
439
- declare function warmSvg(nodes: readonly SvgNode[]): Promise<void>;
475
+ declare function warmSvg(nodes: readonly SvgNode[], opts?: Pick<SerializeOptions, 'textLayout'>): Promise<void>;
440
476
 
441
477
  /**
442
478
  * Map between SVG `<pattern>` elements and weasel's pattern paints.
@@ -502,6 +538,12 @@ interface SvgKitLeafData {
502
538
  style?: TextStyle;
503
539
  runs?: readonly StyledRun[];
504
540
  verticalAlign?: TextVerticalAlign;
541
+ /** A text node's paint over its whole box, under the glyphs. */
542
+ background?: Fill | null;
543
+ /** A text node's inset from its box to its text, on every side. */
544
+ padding?: number;
545
+ /** `'clip'` cuts a text node's text at its box. */
546
+ overflow?: 'visible' | 'clip';
505
547
  image?: ImageNodeData['image'];
506
548
  }
507
549
  /** A kit paint as an `SvgPaint`. `null` is `fill="none"`; a box-relative
@@ -517,19 +559,23 @@ declare function svgImageFromKit(image: ImageNodeData['image'], pose: SvgKitPose
517
559
  /**
518
560
  * Write one leaf as the `SvgNode` its built-in painter draws — image, text or
519
561
  * path, in the order the painters claim a node. `null` for data none of them
520
- * draws.
562
+ * draws. A text node with a `background` is a group: the box, then the text.
563
+ * One that clips is a group clipped to its box as it is drawn, turned with it.
521
564
  *
522
565
  * A path with no `fill` takes `kit:path`'s fallback: the default shape fill
523
566
  * when it has no stroke, and no fill when it has one. Under a pose `transform`
524
567
  * its stroke width takes the uniform part of the stretch, as `parseSvg` reads
525
568
  * a stroke under a skewed element.
526
569
  */
527
- declare function svgLeafFromKit(data: SvgKitLeafData, pose: SvgKitPose): SvgPathNode | SvgTextNode | SvgImageNode | null;
570
+ declare function svgLeafFromKit(data: SvgKitLeafData, pose: SvgKitPose): SvgPathNode | SvgTextNode | SvgImageNode | SvgGroupNode | null;
528
571
  /** One node as {@link svgNodesFromKit} reads it. */
529
572
  interface SvgKitTreeNode {
530
573
  kind: string;
531
574
  data: unknown;
532
575
  pose: SvgKitPose;
576
+ /** The kit's `SceneNode.hidden`: written with `display="none"`, which
577
+ * `parseSvg(…, { keepHidden: true })` reads back as `hidden`. */
578
+ hidden?: boolean;
533
579
  /** A container's copies, as the kit's `ContainerNode.copiesFromPose`
534
580
  * draws them: each is written as a transformed group of its children. */
535
581
  copiesFromPose?(pose: SvgKitPose, node: never): readonly Matrix[] | null;
@@ -556,7 +602,7 @@ interface SvgNodesFromKitOptions<TId extends string = string> {
556
602
  * Walk a scene's container tree into `SvgNode`s ready for `serializeSvg`:
557
603
  * every `container` becomes a group of its children, in z-order, and every
558
604
  * leaf goes through {@link svgLeafFromKit} unless `options.leaf` says
559
- * otherwise.
605
+ * otherwise. A hidden node is written hidden, with its subtree as it is.
560
606
  */
561
607
  declare function svgNodesFromKit<TId extends string>(tree: SvgKitTree<TId>, options?: SvgNodesFromKitOptions<TId>): SvgNode[];
562
608
 
@@ -611,6 +657,10 @@ interface SvgNodesToKitDraftsOptions<TData> {
611
657
  pose: SvgDraftPose;
612
658
  data: SvgKitLeafData;
613
659
  }, source: SvgLeafNode): TData | null;
660
+ /** Keep a group that draws nothing, which is otherwise dropped — for one
661
+ * whose metadata means something with no children under it. It is drafted
662
+ * with its clip's box, or a zero box at the origin. */
663
+ keepEmpty?(source: SvgGroupNode): boolean;
614
664
  }
615
665
  /** Lower an `SvgPaint` onto the paint of the `kit:path` painter's
616
666
  * `data.fill` — a `Paint`, or `null` for SVG's `fill="none"` (the painter
@@ -628,7 +678,8 @@ declare function strokeDataFromSvg(stroke: SvgStroke | undefined, box: SvgDraftB
628
678
  * Walk an `SvgNode[]` tree and emit a flat, parent-before-child list of
629
679
  * {@link SvgSceneDraft}s. Each `<g>` becomes a container whose pose is the
630
680
  * union AABB of its descendants, or its clip's box when it has one; leaves
631
- * carry kit-painter-native data. Empty groups are dropped.
681
+ * carry kit-painter-native data. Empty groups are dropped unless
682
+ * `options.keepEmpty` keeps them, and so is a group holding only those.
632
683
  *
633
684
  * Handed a whole `ParseResult`, it also registers the document's markers
634
685
  * (`parsed.markers`), without which the strokes naming them draw bare.
@@ -646,9 +697,10 @@ declare function svgNodesToKitDrafts<TData = Record<string, unknown>>(input: Par
646
697
  * Each file is fit-clamped to 90% of the visible viewport and centered on the
647
698
  * drop point (or the viewport center), with later files cascading by a fixed
648
699
  * offset. A multi-root file is wrapped in one container so it arrives as a
649
- * single selectable unit. A file that fails to parse, or parses to nothing,
700
+ * single selectable unit. Content the file hides arrives hidden
701
+ * (`SceneNode.hidden`) rather than being left out. A file that fails to parse, or parses to nothing,
650
702
  * is skipped with a `console.warn`.
651
703
  */
652
704
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
653
705
 
654
- 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, 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 };