@weasel-js/svg 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @weasel-js/svg
2
+
3
+ SVG import/export for @weasel-js/core: parse SVG strings into weasel-native shapes and serialize them back.
4
+
5
+ Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
6
+ scene-graph canvas kit for React. See the
7
+ [API reference](https://orochi235.github.io/weasel/api/).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @weasel-js/svg
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { /* … */ } from '@weasel-js/svg';
19
+ ```
20
+
21
+ ## License
22
+
23
+ MIT
@@ -0,0 +1,307 @@
1
+ import { Path, FillStyle, StyledRun, TextStyle, IngestCtx } from '@weasel-js/core';
2
+
3
+ /**
4
+ * Public types for `@weasel-js/svg`. The package exposes a flat,
5
+ * discriminated-union node model (`SvgNode`) that mirrors the SVG element
6
+ * tree but uses weasel-native leaf data (`Path`, `FillStyle`) for geometry and
7
+ * paint.
8
+ *
9
+ * Parsing collapses every `transform="..."` onto its descendants' geometry,
10
+ * so the `SvgNode` tree returned from `parseSvg` never has a non-identity
11
+ * group transform. Consumers may still construct groups with explicit
12
+ * transforms before serializing, in which case the serializer emits a
13
+ * single `matrix(a b c d e f)` on the `<g>`.
14
+ */
15
+
16
+ /**
17
+ * Opaque pass-through bag for namespaced XML content.
18
+ *
19
+ * weasel-svg does not interpret the contents — it only ensures that any
20
+ * attributes or child elements in a *declared* XML namespace round-trip
21
+ * losslessly through parse → serialize. Consumers (e.g. an app-specific
22
+ * bridge layer) hang their domain semantics off this structure.
23
+ *
24
+ * Keyed by namespace prefix (the prefix is a write-time choice; the URI
25
+ * is the canonical identifier and is supplied via `ParseOptions.namespaces`
26
+ * / `SerializeOptions.namespaces`).
27
+ */
28
+ interface NamespaceMeta {
29
+ [prefix: string]: {
30
+ /** Local-name → string-value map for attributes in this namespace. */
31
+ attrs?: Record<string, string>;
32
+ /**
33
+ * Child elements in this namespace, keyed by local name. Each entry
34
+ * is an array because a namespace can host multiple sibling elements
35
+ * with the same tag (e.g. `<wd:layer/><wd:layer/>`).
36
+ */
37
+ elements?: Record<string, NamespacedElement[]>;
38
+ };
39
+ }
40
+ /** Opaque structured representation of a namespaced element. */
41
+ interface NamespacedElement {
42
+ /** Attribute local-name → string-value. */
43
+ attrs: Record<string, string>;
44
+ /** Text content, when the element contains only text (no child elements). */
45
+ text?: string;
46
+ /** Nested namespaced children, keyed by local name (recursive). */
47
+ children?: Record<string, NamespacedElement[]>;
48
+ }
49
+ /**
50
+ * 2x3 affine matrix in column-major form (SVG's `matrix(a b c d e f)`
51
+ * order). Maps `[x', y'] = [a*x + c*y + e, b*x + d*y + f]`.
52
+ */
53
+ type Matrix = readonly [number, number, number, number, number, number];
54
+ /** Identity matrix — useful as a default in tests / constructors. */
55
+ declare const IDENTITY_MATRIX: Matrix;
56
+ /**
57
+ * FillStyle description in the SVG sense — either explicit `none`, a solid
58
+ * color, or a reference to a weasel-native gradient `FillStyle`. Solid colors
59
+ * are normalized to `#rrggbb` strings; opacity is carried separately so
60
+ * `fill-opacity` and `stroke-opacity` round-trip cleanly.
61
+ */
62
+ type SvgPaint = {
63
+ kind: 'none';
64
+ } | {
65
+ kind: 'solid';
66
+ color: string;
67
+ opacity?: number;
68
+ } | {
69
+ kind: 'gradient';
70
+ paint: FillStyle;
71
+ };
72
+ /** Stroke description: a paint plus structural line parameters. */
73
+ interface SvgStroke {
74
+ paint: SvgPaint;
75
+ width: number;
76
+ opacity?: number;
77
+ /** `stroke-linecap`. Default per SVG spec is `'butt'`. */
78
+ cap?: 'butt' | 'round' | 'square';
79
+ /** `stroke-linejoin`. Default per SVG spec is `'miter'`. SVG's `arcs` / `miter-clip` map to `'miter'` with a warning. */
80
+ join?: 'miter' | 'round' | 'bevel';
81
+ /** `stroke-dasharray` as a flat number array. Odd-length inputs are doubled per SVG spec. */
82
+ dash?: number[];
83
+ /**
84
+ * `stroke-miterlimit`. SVG's default is 4. Weasel's renderer defaults to
85
+ * 10 (Canvas2D) when unset, so parsed strokes without an explicit
86
+ * attribute may render with longer miters than the source SVG intended.
87
+ */
88
+ miterLimit?: number;
89
+ }
90
+ /**
91
+ * Leaf node: a path geometry plus fill/stroke. All other v1 shapes
92
+ * (`<rect>`, `<ellipse>`, etc.) are lowered to this representation on
93
+ * parse — `<rect>` uses weasel's `RectPath` fast-path subtype when
94
+ * possible, everything else becomes a `PolygonPath`.
95
+ */
96
+ interface SvgPathNode {
97
+ kind: 'path';
98
+ path: Path;
99
+ fill: SvgPaint;
100
+ stroke?: SvgStroke;
101
+ /** Element-level opacity (`opacity="..."`), 0..1. */
102
+ opacity?: number;
103
+ /** Element-level rotation in **radians**, pivoting around the AABB
104
+ * center of the underlying `path` geometry. Emitted as SVG
105
+ * `transform="rotate(angleDegrees cx cy)"`. */
106
+ rotation?: number;
107
+ /** Opaque per-element bag for declared namespaces. See `NamespaceMeta`. */
108
+ meta?: NamespaceMeta;
109
+ }
110
+ /**
111
+ * Group node: an SVG `<g>`. On parse, `transform` is always omitted (any
112
+ * `transform` attribute is collapsed onto descendants' geometry). On
113
+ * serialize, a non-identity `transform` is emitted as
114
+ * `matrix(a b c d e f)`.
115
+ */
116
+ interface SvgGroupNode {
117
+ kind: 'group';
118
+ children: SvgNode[];
119
+ transform?: Matrix;
120
+ opacity?: number;
121
+ /** Opaque per-element bag for declared namespaces. See `NamespaceMeta`. */
122
+ meta?: NamespaceMeta;
123
+ }
124
+ /**
125
+ * Text leaf node. Mirrors the kit's `TextPose` shape — a bounding box plus
126
+ * text content, optional rich-text `runs` (per-range styling), and an
127
+ * optional `TextStyle` for the node-wide defaults.
128
+ *
129
+ * Note on SVG text geometry: native `<text>` flows from a baseline; it
130
+ * doesn't have width/height attributes. The serializer emits
131
+ * `dominant-baseline="text-before-edge"` so `y` is the top of the text
132
+ * box, plus `data-weasel-width` / `data-weasel-height` so weasel's
133
+ * explicit box dimensions round-trip losslessly. External SVG text
134
+ * (lacking the data-* attrs) imports with estimated dimensions —
135
+ * `width = 99999` (effectively unbounded so wrap doesn't fire) and
136
+ * `height = fontSize * lineHeight * (lineCount || 1)`.
137
+ */
138
+ interface SvgTextNode {
139
+ kind: 'text';
140
+ x: number;
141
+ y: number;
142
+ width: number;
143
+ height: number;
144
+ /** Plain text. When `runs` is set, `runsToPlainText(runs)` must equal `text`. */
145
+ text: string;
146
+ /** Optional per-range styling — same shape as `TextPose.runs`. */
147
+ runs?: StyledRun[];
148
+ /** Node-wide style. Defaults applied at render time via `resolveTextStyle`. */
149
+ style?: TextStyle;
150
+ /** Element-level opacity (`opacity="..."`), 0..1. */
151
+ opacity?: number;
152
+ /** Element-level rotation in **radians**, pivoting around the unrotated
153
+ * AABB center `(x + width/2, y + height/2)`. Emitted as SVG
154
+ * `transform="rotate(angleDegrees cx cy)"`. */
155
+ rotation?: number;
156
+ /** Opaque per-element bag for declared namespaces. See `NamespaceMeta`. */
157
+ meta?: NamespaceMeta;
158
+ }
159
+ /** Discriminated-union node — the leaf of the public tree. */
160
+ type SvgNode = SvgPathNode | SvgGroupNode | SvgTextNode;
161
+ /** Options for {@link parseSvg}. */
162
+ interface ParseOptions {
163
+ /**
164
+ * Map of prefix → URI for XML namespaces the caller wants surfaced.
165
+ * Attributes / child elements in any declared namespace are collected
166
+ * into `SvgNode.meta` (per element) or `ParseResult.documentMeta` (root).
167
+ * Undeclared namespaces are preserved in the DOM but not promoted into
168
+ * the structured `meta` bag; they are silently dropped at serialize time.
169
+ */
170
+ namespaces?: Record<string, string>;
171
+ }
172
+ /** Output of {@link parseSvg}. */
173
+ interface ParseResult {
174
+ nodes: SvgNode[];
175
+ /** Non-fatal notices (unsupported elements, unrecognized attributes). */
176
+ warnings: string[];
177
+ /**
178
+ * Opaque per-document bag for declared namespaces. Holds root-level
179
+ * namespaced attributes (`documentMeta.<prefix>.attrs`) and namespaced
180
+ * root children (`documentMeta.<prefix>.elements`).
181
+ */
182
+ documentMeta?: NamespaceMeta;
183
+ /** Root `viewBox` attribute, parsed from `"x y width height"`. */
184
+ viewBox?: {
185
+ x: number;
186
+ y: number;
187
+ width: number;
188
+ height: number;
189
+ };
190
+ /** Root `width` attribute, parsed as a unitless number when present. */
191
+ width?: number;
192
+ /** Root `height` attribute, parsed as a unitless number when present. */
193
+ height?: number;
194
+ /** Text content of the first `<title>` child of `<svg>`, when present. */
195
+ title?: string;
196
+ }
197
+ /** Options for {@link serializeSvg}. */
198
+ interface SerializeOptions {
199
+ /**
200
+ * Override the root `viewBox`. When omitted, the serializer computes
201
+ * a tight bounding box from the supplied nodes.
202
+ */
203
+ viewBox?: {
204
+ x: number;
205
+ y: number;
206
+ width: number;
207
+ height: number;
208
+ };
209
+ /** Emit `width="..."` on the root `<svg>`. */
210
+ width?: number;
211
+ /** Emit `height="..."` on the root `<svg>`. */
212
+ height?: number;
213
+ /**
214
+ * Emit `<title>...</title>` as the first child of the root `<svg>`. The
215
+ * value is XML-escaped on output. Empty strings are skipped.
216
+ */
217
+ title?: string;
218
+ /**
219
+ * Map of prefix → URI for XML namespaces to declare on the root
220
+ * `<svg>`. The serializer reads `documentMeta[prefix]` and
221
+ * `node.meta[prefix]` to write the actual attributes and elements.
222
+ */
223
+ namespaces?: Record<string, string>;
224
+ /**
225
+ * Opaque per-document namespaced extras. Each `<prefix>.attrs` becomes
226
+ * `<prefix>:<name>="..."` attributes on the root `<svg>`. Each
227
+ * `<prefix>.elements[localName]` becomes `<prefix>:<localName>...>`
228
+ * children placed immediately before any geometry.
229
+ */
230
+ documentMeta?: NamespaceMeta;
231
+ /** Pretty-print with newlines + indentation. Default `false`. */
232
+ pretty?: boolean;
233
+ }
234
+
235
+ /**
236
+ * Parse an SVG document string into a `SvgNode[]` tree. Walks the SVG
237
+ * DOM (via the platform's `DOMParser` — jsdom in tests, the browser at
238
+ * runtime) and lowers each supported element to weasel-native shapes.
239
+ *
240
+ * Transforms are pushed onto a matrix stack and collapsed into leaf
241
+ * geometry at the moment a leaf is created, so the output tree never
242
+ * carries `transform` data — even on `<g>` nodes.
243
+ */
244
+
245
+ /**
246
+ * Public entry point: parse an SVG document string. Errors during DOM
247
+ * parsing produce a `ParseResult` with an empty `nodes` array and a
248
+ * warning describing the issue, rather than throwing.
249
+ */
250
+ declare function parseSvg(svg: string, opts?: ParseOptions): ParseResult;
251
+
252
+ /**
253
+ * Serialize a `SvgNode[]` tree to an SVG document string. Every leaf is
254
+ * emitted as a `<path>` (even shapes that started as `<rect>` etc.) —
255
+ * lossless geometry round-trip beats round-tripping the element-kind
256
+ * label. Gradient paints are gathered into a single `<defs>` block.
257
+ */
258
+
259
+ /**
260
+ * Walk the tree and produce an SVG document string. The root `<svg>`'s
261
+ * `viewBox` is taken from `opts.viewBox` if supplied, otherwise computed
262
+ * as a tight bounding box around every leaf.
263
+ */
264
+ declare function serializeSvg(nodes: SvgNode[], opts?: SerializeOptions): string;
265
+
266
+ interface SvgDraftBounds {
267
+ x: number;
268
+ y: number;
269
+ width: number;
270
+ height: number;
271
+ }
272
+ type DraftPose = SvgDraftBounds & {
273
+ rotation?: number;
274
+ };
275
+ /**
276
+ * One node the unpack wants the scene to materialize, in parent-before-child
277
+ * order (a draft's `parentId` always names an earlier draft, or `null` for
278
+ * roots). Leaf `data` is kit-painter-native (see module doc).
279
+ */
280
+ type SvgSceneDraft = {
281
+ kind: 'container';
282
+ id: string;
283
+ parentId: string | null;
284
+ pose: DraftPose;
285
+ } | {
286
+ kind: 'leaf';
287
+ id: string;
288
+ parentId: string | null;
289
+ pose: DraftPose;
290
+ data: Record<string, unknown>;
291
+ };
292
+ /**
293
+ * Walk an `SvgNode[]` tree and emit a flat, parent-before-child list of
294
+ * {@link SvgSceneDraft}s. Each `<g>` becomes a container whose pose is the
295
+ * union AABB of its descendants (the kit `group` action's convention);
296
+ * path/text leaves carry kit-painter-native data. Empty groups are dropped.
297
+ */
298
+ declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => string): SvgSceneDraft[];
299
+ /**
300
+ * Parse each file and insert its node tree — one undoable `applyOps` batch
301
+ * per file. See the module doc for placement and wrapping policy. A file
302
+ * that fails to parse (or parses to nothing) is skipped with a
303
+ * `console.warn`; the rest proceed.
304
+ */
305
+ declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
306
+
307
+ export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, svgNodesToKitDrafts, unpackSvgFiles };