@weasel-js/svg 1.11.1 → 1.13.0-rc.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/README.md CHANGED
@@ -105,13 +105,16 @@ drafts the kit's built-in path, text and image painters draw, and registers
105
105
  the document's markers. Containers carry no opacity, so an element or group
106
106
  `opacity` is multiplied into the paints of the leaves under it. `nextId` is
107
107
  handed the node each id is for, and `options.leaf` maps each leaf's kit data
108
- into data of your own shape, from the source node's metadata too.
108
+ into data of your own shape, from the source node's metadata too. `options.mark`
109
+ says which `<g>`s are marks with children, as the export's `mark` decorator
110
+ tagged them.
109
111
 
110
112
  `svgNodesFromKit(scene)` goes the other way: containers become groups, and
111
113
  each leaf is written the way its painter draws it, with the pose baked into
112
114
  the geometry and box-relative paints resolved into that box. Hand
113
115
  `serializeSvg` the result. Options pick the roots, skip nodes (a hidden
114
- layer), replace a leaf's lowering or decorate a group. The per-leaf pieces are
116
+ layer), replace a leaf's lowering, or decorate a group or a mark with children
117
+ so the import can tell the two apart. The per-leaf pieces are
115
118
  public too: `svgLeafFromKit`, `svgPaintFromKit`, `svgStrokeFromKit`,
116
119
  `svgImageFromKit`.
117
120
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Affine } from '@weasel-js/geom';
2
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
+ import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, VertexWidth, StrokeNib, StyledRun, TextStyle, TextVerticalAlign, Fill, Stroke, PoseLinear, MarkerEntry, TextPoseLayout, RenderNeeds, TilePatternSpec, IngestCtx, Inherit, ImageNodeData, NodeClip } from '@weasel-js/core';
4
4
 
5
5
  /**
6
6
  * Media Queries 4 evaluation against a fixed environment.
@@ -134,6 +134,17 @@ interface SvgStroke {
134
134
  vertexWidths?: VertexWidth[];
135
135
  nib?: StrokeNib;
136
136
  }
137
+ /**
138
+ * Which of a node's paints its own element sets none of, so that it takes the
139
+ * paint of the enclosing `<g>` that does. Its `fill` and `stroke` still hold
140
+ * what that resolves to.
141
+ */
142
+ interface SvgInherits {
143
+ fill?: true;
144
+ /** Set only when the element sets no `stroke-*` or `marker-*` property
145
+ * either: the stroke is taken whole. */
146
+ stroke?: true;
147
+ }
137
148
  /**
138
149
  * Leaf node: a path geometry plus fill/stroke. Other shapes (`<rect>`,
139
150
  * `<ellipse>`, etc.) are lowered to this on parse.
@@ -143,6 +154,7 @@ interface SvgPathNode {
143
154
  path: Path;
144
155
  fill: SvgPaint;
145
156
  stroke?: SvgStroke;
157
+ inherits?: SvgInherits;
146
158
  /** Element-level opacity (`opacity="..."`), 0..1. */
147
159
  opacity?: number;
148
160
  /** Element-level rotation in **radians**, pivoting around the AABB
@@ -172,6 +184,12 @@ interface SvgGroupNode {
172
184
  * unions them and a `Path` holds one outline. */
173
185
  clip?: Path;
174
186
  opacity?: number;
187
+ /** The fill its children take when they set none — written as the group's
188
+ * own `fill`. Absent when neither it nor an enclosing group sets one. */
189
+ fill?: SvgPaint;
190
+ /** As `fill`, for the stroke; absent too when the one in effect is none. */
191
+ stroke?: SvgStroke;
192
+ inherits?: SvgInherits;
175
193
  /** Kept by `ParseOptions.keepHidden` from content SVG does not render:
176
194
  * `display="none"` here, or a leaf under `visibility="hidden"`. Written
177
195
  * back as `display="none"`. */
@@ -218,6 +236,7 @@ interface SvgTextNode {
218
236
  */
219
237
  fill?: Fill | null;
220
238
  stroke?: Stroke;
239
+ inherits?: SvgInherits;
221
240
  /** Element-level opacity (`opacity="..."`), 0..1. */
222
241
  opacity?: number;
223
242
  /** Element-level rotation in **radians**, pivoting around the unrotated
@@ -491,6 +510,21 @@ declare function tilePreviewSvg(spec: TilePatternSpec, background?: string): str
491
510
  /** `tilePreviewSvg` packed as a `url(...)` value for CSS `background-image`. */
492
511
  declare function tilePreviewCssUrl(spec: TilePatternSpec, background?: string): string;
493
512
 
513
+ /** CSS Selectors 4 specificity. */
514
+ /** A selector's specificity as (ids, classes, types). */
515
+ type Specificity = readonly [number, number, number];
516
+ /** Negative when `a` is less specific than `b`, positive when more, 0 when equal. */
517
+ declare function compareSpecificity(a: Specificity, b: Specificity): number;
518
+ /** CSS Selectors 4 specificity of one complex selector (no comma list). */
519
+ declare function specificity(selector: string): Specificity;
520
+
521
+ /**
522
+ * Split `s` on `sep` wherever it sits outside strings and (), [] nesting.
523
+ * With `dropBlocks`, a `{…}` block at depth zero is discarded along with the
524
+ * prelude before it, back to the previous separator.
525
+ */
526
+ declare function splitTopLevel(s: string, sep: string, dropBlocks?: boolean): string[];
527
+
494
528
  /**
495
529
  * `@supports` evaluation. A declaration test asks whether this parser would
496
530
  * honor the declaration ({@link isDeclarationHonored}), not whether a browser
@@ -508,6 +542,19 @@ interface SupportsOptions {
508
542
  */
509
543
  declare function evaluateSupports(condition: string, opts?: SupportsOptions): boolean;
510
544
 
545
+ /**
546
+ * Parse each file and insert its node tree, one undo step per file. Pass it
547
+ * as `<SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }}>`.
548
+ *
549
+ * Each file is fit-clamped to 90% of the visible viewport and centered on the
550
+ * drop point (or the viewport center), with later files cascading by a fixed
551
+ * offset. A multi-root file is wrapped in one container so it arrives as a
552
+ * single selectable unit. Content the file hides arrives hidden
553
+ * (`SceneNode.hidden`) rather than being left out. A file that fails to parse, or parses to nothing,
554
+ * is skipped with a `console.warn`.
555
+ */
556
+ declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
557
+
511
558
  /**
512
559
  * The kit-to-SVG direction: scene leaves back to `SvgNode`s, the inverse of
513
560
  * `svgNodesToKitDrafts`. Each leaf is written as the built-in painter that
@@ -532,8 +579,9 @@ interface SvgKitPose {
532
579
  * `svgNodesToKitDrafts` writes. */
533
580
  interface SvgKitLeafData {
534
581
  path?: Path;
535
- fill?: Fill | null;
536
- stroke?: Stroke | null;
582
+ /** `'inherit'` takes the enclosing container's. */
583
+ fill?: Fill | Inherit | null;
584
+ stroke?: Stroke | Inherit | null;
537
585
  text?: string;
538
586
  style?: TextStyle;
539
587
  runs?: readonly StyledRun[];
@@ -552,7 +600,7 @@ interface SvgKitLeafData {
552
600
  declare function svgPaintFromKit(fill: Paint | null, box: SvgKitPose): SvgPaint;
553
601
  /** A kit `Stroke` as an `SvgStroke` — the inverse of `strokeDataFromSvg`.
554
602
  * One with no paint or no width draws nothing and is written as no stroke. */
555
- declare function svgStrokeFromKit(stroke: Stroke | null | undefined, box: SvgKitPose): SvgStroke | undefined;
603
+ declare function svgStrokeFromKit(raw: Stroke | null | undefined, box: SvgKitPose): SvgStroke | undefined;
556
604
  /** Write a `kit:image` leaf as an `SvgImageNode` — the inverse of the image
557
605
  * arm of `svgNodesToKitDrafts`. The pose is the image's box. */
558
606
  declare function svgImageFromKit(image: ImageNodeData['image'], pose: SvgKitPose): SvgImageNode;
@@ -562,10 +610,12 @@ declare function svgImageFromKit(image: ImageNodeData['image'], pose: SvgKitPose
562
610
  * draws. A text node with a `background` is a group: the box, then the text.
563
611
  * One that clips is a group clipped to its box as it is drawn, turned with it.
564
612
  *
565
- * A path with no `fill` takes `kit:path`'s fallback: the default shape fill
566
- * when it has no stroke, and no fill when it has one. Under a pose `transform`
567
- * its stroke width takes the uniform part of the stretch, as `parseSvg` reads
568
- * a stroke under a skewed element.
613
+ * An `'inherit'` fill or stroke is none here, with no container to read;
614
+ * {@link svgNodesFromKit} resolves it first.
615
+ *
616
+ * A fill or stroke that paints nothing is written `none`. Under a pose
617
+ * `transform` a stroke's width takes the uniform part of the stretch, as
618
+ * `parseSvg` reads a stroke under a skewed element.
569
619
  */
570
620
  declare function svgLeafFromKit(data: SvgKitLeafData, pose: SvgKitPose): SvgPathNode | SvgTextNode | SvgImageNode | SvgGroupNode | null;
571
621
  /** One node as {@link svgNodesFromKit} reads it. */
@@ -576,8 +626,11 @@ interface SvgKitTreeNode {
576
626
  /** The kit's `SceneNode.hidden`: written with `display="none"`, which
577
627
  * `parseSvg(…, { keepHidden: true })` reads back as `hidden`. */
578
628
  hidden?: boolean;
579
- /** A container's copies, as the kit's `ContainerNode.copiesFromPose`
580
- * draws them: each is written as a transformed group of its children. */
629
+ /** The kit's `SceneNode.clip`: `'outline'` writes the node's children
630
+ * clipped to its outline, as core's `clipOutline` reads it. */
631
+ clip?: NodeClip;
632
+ /** The node's copies, as the kit's `SceneNode.copiesFromPose` draws them:
633
+ * each is written as a transformed group of its children. */
581
634
  copiesFromPose?(pose: SvgKitPose, node: never): readonly Matrix[] | null;
582
635
  }
583
636
  /** The read side of a scene that {@link svgNodesFromKit} walks. A kit
@@ -593,18 +646,37 @@ interface SvgNodesFromKitOptions<TId extends string = string> {
593
646
  roots?: readonly TId[];
594
647
  /** `false` leaves a node out, with everything under it — a hidden layer. */
595
648
  include?(id: TId): boolean;
596
- /** Lower a leaf yourself; `null` leaves it out. Defaults to {@link svgLeafFromKit}. */
597
- leaf?(id: TId, node: SvgKitTreeNode): SvgNode | null;
598
- /** Decorate the group written for a container, e.g. with `meta`. */
649
+ /** Lower a leaf yourself; `null` leaves it out. Defaults to {@link svgLeafFromKit}.
650
+ * `inherited` is what the leaf's `'inherit'` fields resolve to; write those
651
+ * as these, and the walk marks the result as inheriting where SVG's
652
+ * cascade can carry them. */
653
+ leaf?(id: TId, node: SvgKitTreeNode, inherited: SvgKitInherited): SvgNode | null;
654
+ /** Decorate the `<g>` written for a scene group, e.g. with `meta`. */
599
655
  group?(id: TId, group: SvgGroupNode): SvgGroupNode;
656
+ /** Decorate the `<g>` written for a mark with children, so a reader can
657
+ * tell it from a group: `svgNodesToKitDrafts`'s `mark` option reads one
658
+ * back. Given, the `<g>` holds the mark's own element, then one `<g>` of its
659
+ * children carrying the mark's clip, then a transformed `<g>` of that for
660
+ * each copy; a mark whose children are all left out holds its own element
661
+ * alone. Not called for a mark whose own element is itself a group, or that
662
+ * draws nothing. */
663
+ mark?(id: TId, group: SvgGroupNode): SvgGroupNode;
600
664
  }
601
665
  /**
602
- * Walk a scene's container tree into `SvgNode`s ready for `serializeSvg`:
603
- * every `container` becomes a group of its children, in z-order, and every
604
- * leaf goes through {@link svgLeafFromKit} unless `options.leaf` says
605
- * otherwise. A hidden node is written hidden, with its subtree as it is.
666
+ * Walk a scene's tree into `SvgNode`s ready for `serializeSvg`: a group
667
+ * becomes a `<g>` of its children, in z-order, drawing nothing of its own; a
668
+ * mark is written by {@link svgLeafFromKit} unless `options.leaf` says
669
+ * otherwise, and a mark with children is a `<g>` holding that element and then
670
+ * its children, or one `<g>` of them when `options.mark` decorates it. A hidden
671
+ * node is written hidden, with its subtree as it is.
606
672
  */
607
673
  declare function svgNodesFromKit<TId extends string>(tree: SvgKitTree<TId>, options?: SvgNodesFromKitOptions<TId>): SvgNode[];
674
+ /** What a leaf's `'inherit'` fill and stroke resolve to — its nearest
675
+ * enclosing container's; `null` for none. */
676
+ interface SvgKitInherited {
677
+ fill: Fill | null;
678
+ stroke: Stroke | null;
679
+ }
608
680
 
609
681
  /** Axis-aligned box, in the coordinate space of the SVG being unpacked. Used
610
682
  * for the bounds a draft occupies before it becomes a scene node. */
@@ -620,30 +692,45 @@ type SvgDraftPose = SvgDraftBounds & {
620
692
  transform?: PoseLinear;
621
693
  };
622
694
 
695
+ /**
696
+ * Parse an SVG file into **native scene nodes** — path/text leaves under
697
+ * containers that mirror the source `<g>` structure — instead of the default
698
+ * single embedded-image node.
699
+ *
700
+ * Leaf data targets the kit's built-in painters. Poses are absolute AABBs, and
701
+ * the renderer rebases stored geometry into the pose box, so placement and
702
+ * fit-clamping operate on poses alone and never rewrite path coordinates.
703
+ * Stroke widths, dashes, font sizes, and letter spacing live in data, so
704
+ * they take the clamp separately.
705
+ */
706
+
623
707
  /**
624
708
  * One node the unpack wants the scene to materialize, in parent-before-child
625
709
  * order (a draft's `parentId` always names an earlier draft, or `null` for
626
710
  * roots). `hidden` is the source node's, for the caller to map onto whatever
627
711
  * it hides a node with.
628
712
  *
629
- * A container's `data` is empty unless its `<g>` is clipped. A scene container
630
- * clips to what its painter draws, so a clipped one holds the clip as an
631
- * unpainted path, `{ path, fill: null }`, and its `pose` is the clip's box
632
- * rather than its children's.
713
+ * A group's `data` is empty unless its `<g>` is clipped. A clipped one declares
714
+ * `clip: 'outline'` and holds the clip as an unpainted path,
715
+ * `{ path, fill: null }`, which is the outline a group clips to; its `pose` is
716
+ * the clip's box rather than its children's. A mark declares a clip only when
717
+ * it is read from a `<g>` `options.mark` claims.
633
718
  */
634
719
  type SvgSceneDraft<TData = Record<string, unknown>> = {
635
- kind: 'container';
720
+ kind: 'group';
636
721
  id: string;
637
722
  parentId: string | null;
638
723
  pose: SvgDraftPose;
639
724
  data: SvgKitLeafData;
725
+ clip?: NodeClip;
640
726
  hidden?: true;
641
727
  } | {
642
- kind: 'leaf';
728
+ kind: 'mark';
643
729
  id: string;
644
730
  parentId: string | null;
645
731
  pose: SvgDraftPose;
646
732
  data: TData;
733
+ clip?: NodeClip;
647
734
  hidden?: true;
648
735
  };
649
736
  /** A leaf `SvgNode`: everything but a group. */
@@ -661,6 +748,14 @@ interface SvgNodesToKitDraftsOptions<TData> {
661
748
  * whose metadata means something with no children under it. It is drafted
662
749
  * with its clip's box, or a zero box at the origin. */
663
750
  keepEmpty?(source: SvgGroupNode): boolean;
751
+ /** Whether a `<g>` is a mark with children, as `svgNodesFromKit`'s `mark`
752
+ * option writes one: its first element is drafted as the mark, the
753
+ * children of the `<g>` after it as the mark's, hidden where that `<g>` is,
754
+ * with its clip as the mark's `clip: 'outline'`, and anything after that,
755
+ * such as baked copies, as the mark's further children. A clip on the
756
+ * claimed `<g>` itself clips the mark as a group around it. A `<g>` whose
757
+ * first element is a group, or whose second is not, is a group. */
758
+ mark?(source: SvgGroupNode): boolean;
664
759
  }
665
760
  /** Lower an `SvgPaint` onto the paint of the `kit:path` painter's
666
761
  * `data.fill` — a `Paint`, or `null` for SVG's `fill="none"` (the painter
@@ -690,17 +785,5 @@ declare function strokeDataFromSvg(stroke: SvgStroke | undefined, box: SvgDraftB
690
785
  * data; a group whose leaves it all leaves out is dropped like an empty one.
691
786
  */
692
787
  declare function svgNodesToKitDrafts<TData = Record<string, unknown>>(input: ParseResult | readonly SvgNode[], nextId: (source: SvgNode) => string, options?: SvgNodesToKitDraftsOptions<TData>): SvgSceneDraft<TData>[];
693
- /**
694
- * Parse each file and insert its node tree, one undo step per file. Pass it
695
- * as `<SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }}>`.
696
- *
697
- * Each file is fit-clamped to 90% of the visible viewport and centered on the
698
- * drop point (or the viewport center), with later files cascading by a fixed
699
- * offset. A multi-root file is wrapped in one container so it arrives as a
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,
702
- * is skipped with a `console.warn`.
703
- */
704
- declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
705
788
 
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 };
789
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type Specificity, 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, compareSpecificity, escapeAttr, escapeText, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, specificity, splitTopLevel, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };