@mosaicoo/svg-engine 0.1.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 +201 -0
- package/NOTICE +21 -0
- package/README.md +249 -0
- package/fesm2022/mosaicoo-svg-engine-ai-nlu-ui.mjs +459 -0
- package/fesm2022/mosaicoo-svg-engine-ai-nlu-voice-wasm.mjs +1 -0
- package/fesm2022/mosaicoo-svg-engine-ai-nlu.mjs +11 -0
- package/fesm2022/mosaicoo-svg-engine-core.mjs +3 -0
- package/fesm2022/mosaicoo-svg-engine-edit.mjs +2292 -0
- package/fesm2022/mosaicoo-svg-engine-io.mjs +47 -0
- package/fesm2022/mosaicoo-svg-engine-optimize.mjs +1 -0
- package/fesm2022/mosaicoo-svg-engine-render.mjs +301 -0
- package/fesm2022/mosaicoo-svg-engine-ui.mjs +14236 -0
- package/fesm2022/mosaicoo-svg-engine.mjs +1 -0
- package/package.json +105 -0
- package/types/mosaicoo-svg-engine-ai-nlu-ui.d.ts +416 -0
- package/types/mosaicoo-svg-engine-ai-nlu-voice-wasm.d.ts +175 -0
- package/types/mosaicoo-svg-engine-ai-nlu.d.ts +1834 -0
- package/types/mosaicoo-svg-engine-core.d.ts +5139 -0
- package/types/mosaicoo-svg-engine-edit.d.ts +11922 -0
- package/types/mosaicoo-svg-engine-io.d.ts +476 -0
- package/types/mosaicoo-svg-engine-optimize.d.ts +183 -0
- package/types/mosaicoo-svg-engine-render.d.ts +628 -0
- package/types/mosaicoo-svg-engine-ui.d.ts +6861 -0
- package/types/mosaicoo-svg-engine.d.ts +30 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { SvgDocument, Disposable, SvgNode } from '@mosaicoo/svg-engine/core';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Outcome of an import operation. Either a parsed document with
|
|
6
|
+
* (optional) non-fatal warnings, OR a hard failure with reason.
|
|
7
|
+
*
|
|
8
|
+
* Soft failures (e.g., "skipped a `<script>` for safety") go in
|
|
9
|
+
* `warnings`; they don't abort the import. Hard failures (malformed
|
|
10
|
+
* XML, unsupported root element) produce `{ ok: false, error }`.
|
|
11
|
+
*/
|
|
12
|
+
type ImportResult = {
|
|
13
|
+
readonly ok: true;
|
|
14
|
+
readonly document: SvgDocument;
|
|
15
|
+
readonly warnings: readonly string[];
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly error: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Plugin contribution to the import pipeline (categoria 4 do D-023).
|
|
22
|
+
*
|
|
23
|
+
* **Fields**:
|
|
24
|
+
* - `id`: unique within {@link ImporterRegistry}. Reverse-DNS suggested.
|
|
25
|
+
* - `name`: human-readable label for UIs that surface importer choice.
|
|
26
|
+
* - `mediaTypes`: list of MIME types the importer accepts (used by UI
|
|
27
|
+
* to filter `<input type="file" accept="...">` and auto-pick).
|
|
28
|
+
* - `extensions`: file extensions WITHOUT the dot (`'svg'`, not `'.svg'`).
|
|
29
|
+
* Same usage as `mediaTypes` for file-based UIs.
|
|
30
|
+
* - `import(text)`: parse `text` and return either a document or an
|
|
31
|
+
* error. MUST be pure (no DOM/state mutation) for testability.
|
|
32
|
+
*/
|
|
33
|
+
interface Importer {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly mediaTypes: readonly string[];
|
|
37
|
+
readonly extensions: readonly string[];
|
|
38
|
+
import(text: string): ImportResult;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Plugin contribution to the export pipeline (categoria 5 do D-023).
|
|
42
|
+
*
|
|
43
|
+
* Mirrors {@link Importer} on the way out.
|
|
44
|
+
*
|
|
45
|
+
* **Fields**:
|
|
46
|
+
* - `id`/`name`: identifier + label
|
|
47
|
+
* - `mediaType`: emitted MIME type (e.g., `'image/svg+xml'`)
|
|
48
|
+
* - `extension`: suggested file extension WITHOUT the dot
|
|
49
|
+
* - `export(doc)`: serialize the document. Returns `string` for text
|
|
50
|
+
* formats (SVG), or `Promise<string | Blob>` for binary / async
|
|
51
|
+
* formats (PNG via canvas, PDF, etc. — anything that needs Image
|
|
52
|
+
* loading or worker offload).
|
|
53
|
+
*
|
|
54
|
+
* **Why the union return type**: text-format exporters (SVG) are
|
|
55
|
+
* cheap + synchronous; forcing them to wrap in `Promise` would add
|
|
56
|
+
* noise + slow down the common case. Binary exporters are inherently
|
|
57
|
+
* async due to `Image.onload`. The union lets each format express
|
|
58
|
+
* its natural shape.
|
|
59
|
+
*
|
|
60
|
+
* Consumers check `typeof result === 'string'` for the sync branch
|
|
61
|
+
* and `await result` for the async branch (or use `await Promise.resolve(result)`
|
|
62
|
+
* to handle both uniformly).
|
|
63
|
+
*/
|
|
64
|
+
interface Exporter {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly mediaType: string;
|
|
68
|
+
readonly extension: string;
|
|
69
|
+
export(document: SvgDocument): string | Promise<string | Blob>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Registry of {@link Importer}s (categoria 4 do D-023). Signal-backed,
|
|
74
|
+
* `register()` returns `Disposable`. Plugins contribute formats via
|
|
75
|
+
* `ctx.track(reg.register(importer))`.
|
|
76
|
+
*
|
|
77
|
+
* **Lookup helpers** for file-based UIs:
|
|
78
|
+
* - `byExtension(ext)`: case-insensitive extension match (without dot)
|
|
79
|
+
* - `byMediaType(type)`: exact MIME match
|
|
80
|
+
* - Both return the FIRST matching importer (insertion order tiebreak)
|
|
81
|
+
*
|
|
82
|
+
* **Why insertion-order tiebreak**: when multiple plugins claim the
|
|
83
|
+
* same extension (e.g., two SVG variants), the first-registered wins.
|
|
84
|
+
* Apps decide priority via the order in their `provideSvgEnginePlugin`
|
|
85
|
+
* array. To override a builtin, plug-in your importer BEFORE the
|
|
86
|
+
* builtin plugin.
|
|
87
|
+
*/
|
|
88
|
+
declare class ImporterRegistry {
|
|
89
|
+
private readonly _importers;
|
|
90
|
+
/** Reactive snapshot of all registered importers (insertion order). */
|
|
91
|
+
readonly importers: i0.Signal<readonly Importer[]>;
|
|
92
|
+
register(importer: Importer): Disposable;
|
|
93
|
+
get(id: string): Importer | null;
|
|
94
|
+
byExtension(ext: string): Importer | null;
|
|
95
|
+
byMediaType(mediaType: string): Importer | null;
|
|
96
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ImporterRegistry, never>;
|
|
97
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<ImporterRegistry>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Registry of {@link Exporter}s (categoria 5 do D-023). Same shape as
|
|
101
|
+
* {@link ImporterRegistry}.
|
|
102
|
+
*/
|
|
103
|
+
declare class ExporterRegistry {
|
|
104
|
+
private readonly _exporters;
|
|
105
|
+
readonly exporters: i0.Signal<readonly Exporter[]>;
|
|
106
|
+
register(exporter: Exporter): Disposable;
|
|
107
|
+
get(id: string): Exporter | null;
|
|
108
|
+
byExtension(ext: string): Exporter | null;
|
|
109
|
+
byMediaType(mediaType: string): Exporter | null;
|
|
110
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ExporterRegistry, never>;
|
|
111
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<ExporterRegistry>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Built-in SVG importer (Fase 5-IO). Parses SVG XML text into a
|
|
116
|
+
* {@link SvgDocument} that the editor's model understands.
|
|
117
|
+
*
|
|
118
|
+
* **Sanitization**: the importer never executes any payload from the
|
|
119
|
+
* input. Specifically:
|
|
120
|
+
*
|
|
121
|
+
* - `<script>` elements (anywhere in the tree) are dropped + warned
|
|
122
|
+
* - `on*` event-handler attributes (`onclick`, `onload`, ...) are
|
|
123
|
+
* dropped + warned
|
|
124
|
+
* - `xlink:href` / `href` values starting with `javascript:` are
|
|
125
|
+
* dropped + warned. `data:` URIs are allowed (caller can validate
|
|
126
|
+
* externally if they want stricter policy)
|
|
127
|
+
* - External entity references (`<!ENTITY>` / `&xxe;`) are not
|
|
128
|
+
* processed by the browser's DOMParser when called via `parseFromString`
|
|
129
|
+
* with `'image/svg+xml'`, so XXE is structurally precluded
|
|
130
|
+
*
|
|
131
|
+
* **Unsupported elements** (gradients, filters, masks, clipPaths,
|
|
132
|
+
* markers, defs, use, switch, foreignObject, ...) are skipped with
|
|
133
|
+
* a single per-tag warning so the result is always a "best-effort"
|
|
134
|
+
* approximation — never a hard fail on a partially-supported file.
|
|
135
|
+
*
|
|
136
|
+
* **viewBox**: extracted from the root `<svg>`. If absent, falls back
|
|
137
|
+
* to `width`/`height` attributes; if those are missing too, defaults
|
|
138
|
+
* to `0 0 800 600`.
|
|
139
|
+
*
|
|
140
|
+
* **Pure**: no DOM mutation outside the throwaway parser document, no
|
|
141
|
+
* service injection. Safe to call from a Web Worker (if/when we add
|
|
142
|
+
* that path).
|
|
143
|
+
*/
|
|
144
|
+
declare const svgImporter: Importer;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* **D-097 — merge two `<defs>` fragments by top-level id.**
|
|
148
|
+
*
|
|
149
|
+
* A `<defs>` fragment is the literal inner XML of a `<defs>` block: zero
|
|
150
|
+
* or more top-level reusable elements (`<linearGradient>`, `<radialGradient>`,
|
|
151
|
+
* `<filter>`, `<pattern>`, `<clipPath>`, …), each typically carrying an `id`
|
|
152
|
+
* that shapes reference via `fill="url(#id)"`.
|
|
153
|
+
*
|
|
154
|
+
* Merging is needed whenever imported content (e.g. replacing/editing a Smart
|
|
155
|
+
* Object's contents, D-097) brings its own defs that must join the document's
|
|
156
|
+
* shared defs so the references keep resolving. Naive string concatenation
|
|
157
|
+
* would duplicate definitions every time the same SVG is re-imported/edited;
|
|
158
|
+
* this dedups by id:
|
|
159
|
+
*
|
|
160
|
+
* - **Existing** fragment is kept verbatim (it owns its ids).
|
|
161
|
+
* - From **incoming**, each top-level element is appended ONLY when its `id`
|
|
162
|
+
* is not already present in existing. Id-less elements are always appended.
|
|
163
|
+
*
|
|
164
|
+
* Returns the original `existing` string (reference-preserving) when there is
|
|
165
|
+
* nothing new to add, so callers can cheaply skip a state write. Best-effort:
|
|
166
|
+
* if parsing is unavailable/fails (non-DOM env, malformed fragment) it falls
|
|
167
|
+
* back to plain concatenation rather than throwing.
|
|
168
|
+
*
|
|
169
|
+
* **Known limitation** (engine-wide): ids are document-global and NOT
|
|
170
|
+
* namespaced. Two unrelated SVGs that both use `id="grad"` collide — the first
|
|
171
|
+
* definition wins. Faithful multi-asset isolation would require id-rewriting,
|
|
172
|
+
* out of scope here (matches the additive-import behavior).
|
|
173
|
+
*/
|
|
174
|
+
declare function mergeDefsFragments(existing: string | undefined, incoming: string | undefined): string;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Collect every element `id` defined inside a `<defs>` fragment string.
|
|
178
|
+
* Returns an empty set when the fragment is empty or `DOMParser` is
|
|
179
|
+
* unavailable (non-browser). Best-effort regex fallback if parsing fails.
|
|
180
|
+
*/
|
|
181
|
+
declare function collectDefsIds(defs: string | undefined | null): ReadonlySet<string>;
|
|
182
|
+
/** Result of {@link namespaceCollidingDefs}. */
|
|
183
|
+
interface NamespacedDefs {
|
|
184
|
+
/** The node tree with references to renamed ids rewritten (same ref when none). */
|
|
185
|
+
readonly root: SvgNode;
|
|
186
|
+
/** The defs fragment with colliding ids + their internal refs rewritten. */
|
|
187
|
+
readonly defs: string;
|
|
188
|
+
/** `oldId → newId` for every renamed id (empty when there was no collision). */
|
|
189
|
+
readonly renamed: ReadonlyMap<string, string>;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* **D-101** — namespace the incoming `(root, defs)` so it can be merged into a
|
|
193
|
+
* document that already owns the ids in `taken`, without collision. Only ids in
|
|
194
|
+
* `defs` that are ALSO in `taken` are renamed (to `prefix + id`); every
|
|
195
|
+
* reference to them is rewritten in both the defs fragment and the node tree.
|
|
196
|
+
*
|
|
197
|
+
* Pure: never mutates its inputs. When nothing collides, returns the inputs
|
|
198
|
+
* unchanged (same references) with an empty `renamed` map — so the common
|
|
199
|
+
* single-import path is a no-op.
|
|
200
|
+
*
|
|
201
|
+
* @param root the imported content (typically a group) whose nodes may
|
|
202
|
+
* reference defs ids via style `url(#…)` or `symbol-use`.
|
|
203
|
+
* @param defs the incoming `<defs>` inner fragment (as produced by the importer).
|
|
204
|
+
* @param taken ids already present in the destination document's defs
|
|
205
|
+
* (see {@link collectDefsIds}).
|
|
206
|
+
* @param prefix unique-per-merge prefix applied to colliding ids (caller-supplied
|
|
207
|
+
* so the function stays deterministic / testable).
|
|
208
|
+
*/
|
|
209
|
+
declare function namespaceCollidingDefs(root: SvgNode, defs: string, taken: ReadonlySet<string>, prefix: string): NamespacedDefs;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Built-in SVG exporter (Fase 5-IO). Serializes a {@link SvgDocument}
|
|
213
|
+
* back to SVG XML text with **deterministic** output: byte-for-byte
|
|
214
|
+
* stable for the same input, friendly for diffs / golden-file tests /
|
|
215
|
+
* VCS commits.
|
|
216
|
+
*
|
|
217
|
+
* **Determinism guarantees**:
|
|
218
|
+
*
|
|
219
|
+
* - Element attribute order: fixed per element type (canonical order
|
|
220
|
+
* below) instead of object-iteration order
|
|
221
|
+
* - Numeric formatting: 6 decimal places max, trailing zeros stripped
|
|
222
|
+
* (`Math.round(n * 1e6) / 1e6` → trim trailing zeros). Eliminates
|
|
223
|
+
* floating-point noise like `0.30000000000000004`
|
|
224
|
+
* - Transform: only emitted when not identity (`[1,0,0,1,0,0]`).
|
|
225
|
+
* Default matrix is implicit in the spec — no point bloating output
|
|
226
|
+
* - Style: only emitted when non-empty. Style attributes are emitted
|
|
227
|
+
* as individual presentation attributes (`fill="..."`), not inline
|
|
228
|
+
* CSS (`style="fill:..."`) — easier to query in raw SVG, and
|
|
229
|
+
* identical visual rendering
|
|
230
|
+
*
|
|
231
|
+
* **Indentation**: 2-space, recursive. Tags on their own line for
|
|
232
|
+
* leaf elements; opening + content + closing tags collapsed for
|
|
233
|
+
* single-line text content. Matches Inkscape's default save style.
|
|
234
|
+
*
|
|
235
|
+
* **Pure**: no DOM mutation, no service injection. Worker-safe.
|
|
236
|
+
*/
|
|
237
|
+
declare const svgExporter: Exporter;
|
|
238
|
+
/**
|
|
239
|
+
* **D-086** — Serialize a SINGLE node to its SVG element markup, in
|
|
240
|
+
* isolation (no document wrapper, no `<defs>`). Reuses the exporter's
|
|
241
|
+
* `renderNode` so a node's geometry/attributes/transform serialize
|
|
242
|
+
* exactly as they would inside a full export.
|
|
243
|
+
*
|
|
244
|
+
* **Primary use**: building `<clipPath>` / `<mask>` definitions from a
|
|
245
|
+
* "clipper" shape for the Object ▸ Mask commands (D-086) — the returned
|
|
246
|
+
* markup is the inner geometry placed inside a `<clipPath>` / `<mask>`
|
|
247
|
+
* element. Titles and SMIL are intentionally OFF (a clip/mask def needs
|
|
248
|
+
* pure geometry, not authored names or animation).
|
|
249
|
+
*
|
|
250
|
+
* **Coordinate space**: the node's own `transform` IS emitted, so the
|
|
251
|
+
* markup paints in the same place the node was on the canvas — exactly
|
|
252
|
+
* what `clipPathUnits="userSpaceOnUse"` / `maskUnits="userSpaceOnUse"`
|
|
253
|
+
* expect. Ancestor-group transforms are NOT baked in (caller's
|
|
254
|
+
* responsibility when the node is nested).
|
|
255
|
+
*
|
|
256
|
+
* @returns the element markup (possibly multi-line/indented), or `''`
|
|
257
|
+
* when the node is doc-hidden (`metadata.visible === false`).
|
|
258
|
+
*/
|
|
259
|
+
declare function nodeToSvgMarkup(node: SvgNode): string;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Reference plugin: PNG export via `<canvas>` (Item 6 — débito 5-IO).
|
|
263
|
+
*
|
|
264
|
+
* Serves two purposes:
|
|
265
|
+
* 1. **Real feature**: lets users export their drawing as a raster
|
|
266
|
+
* PNG (handy for slack/discord/preview thumbnails)
|
|
267
|
+
* 2. **Demonstration of the async exporter contract** — shows how a
|
|
268
|
+
* third-party plugin (or built-in) implements `Exporter` returning
|
|
269
|
+
* `Promise<Blob>` instead of synchronous text
|
|
270
|
+
*
|
|
271
|
+
* **Pipeline**:
|
|
272
|
+
* - Serialize the document to SVG text via {@link svgExporter}
|
|
273
|
+
* - Wrap as a `data:image/svg+xml;base64,...` URI
|
|
274
|
+
* - Load it into an `Image` element (the only way browsers convert
|
|
275
|
+
* SVG → raster on the main thread)
|
|
276
|
+
* - Draw to an offscreen `<canvas>` sized to the viewBox
|
|
277
|
+
* - `toBlob('image/png')` extracts the binary PNG
|
|
278
|
+
*
|
|
279
|
+
* **Quality**: the canvas runs at 2× the viewBox resolution to keep
|
|
280
|
+
* the export crisp on retina displays. Configurable in a future
|
|
281
|
+
* polish via constructor arg (kept hard-coded here for simplicity).
|
|
282
|
+
*
|
|
283
|
+
* **Failure modes**: when `Image.onerror` fires (malformed SVG, cross-
|
|
284
|
+
* origin issues) OR `canvas.toBlob` returns `null` (canvas tainted /
|
|
285
|
+
* unsupported), the returned Promise rejects with a descriptive error.
|
|
286
|
+
*
|
|
287
|
+
* **NOT a plugin yet**: this is just the `Exporter` object. The
|
|
288
|
+
* `builtinIoPlugin` could register it, but to demonstrate plugin
|
|
289
|
+
* separation we ship it as a SEPARATE plugin ({@link pngExporterPlugin})
|
|
290
|
+
* so consumers can opt in/out independently of the SVG IO plugin.
|
|
291
|
+
*/
|
|
292
|
+
/**
|
|
293
|
+
* Render a `SvgDocument` to a PNG Blob at the requested `scale`
|
|
294
|
+
* multiplier (1, 2, 3 — or any positive number). Reused by both the
|
|
295
|
+
* `Exporter` interface implementation and the playground's
|
|
296
|
+
* "Export PNG with presets" dialog.
|
|
297
|
+
*
|
|
298
|
+
* Pipeline matches the original `pngExporter`: serialize → base64 →
|
|
299
|
+
* `<img>` load → `<canvas>` paint → `toBlob('image/png')`.
|
|
300
|
+
*/
|
|
301
|
+
declare function renderPng(document: SvgDocument, scale?: number): Promise<Blob>;
|
|
302
|
+
declare const pngExporter: Exporter;
|
|
303
|
+
|
|
304
|
+
/** gzip a UTF-8 string to bytes. Throws if `CompressionStream` is absent. */
|
|
305
|
+
declare function gzipText(text: string): Promise<Uint8Array>;
|
|
306
|
+
/**
|
|
307
|
+
* gunzip bytes back to a UTF-8 string. Accepts an `ArrayBuffer` (e.g.
|
|
308
|
+
* `await file.arrayBuffer()`) or a `Uint8Array`. Throws if
|
|
309
|
+
* `DecompressionStream` is absent, and rejects if `input` is not valid
|
|
310
|
+
* gzip data.
|
|
311
|
+
*/
|
|
312
|
+
declare function gunzipText(input: ArrayBuffer | Uint8Array): Promise<string>;
|
|
313
|
+
/**
|
|
314
|
+
* `Exporter` that serializes a document to SVG (via {@link svgExporter})
|
|
315
|
+
* then gzips it into an `.svgz` Blob. Async (gzip is stream-based), so it
|
|
316
|
+
* follows the same `Promise<Blob>` shape as `pngExporter`.
|
|
317
|
+
*
|
|
318
|
+
* The Blob's MIME stays `image/svg+xml` — that's what SVGZ *is* (a
|
|
319
|
+
* gzip-encoded SVG); the `.svgz` extension is what tags it as compressed
|
|
320
|
+
* to the OS and other tools.
|
|
321
|
+
*/
|
|
322
|
+
declare const svgzExporter: Exporter;
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* **D-110 — Code Generators (Group A).** Contract for a "code output"
|
|
326
|
+
* format that transforms an {@link SvgDocument} into a **string of source
|
|
327
|
+
* code** — React JSX, a React component file, a Data URI, etc. — for
|
|
328
|
+
* pasting into an app or component library.
|
|
329
|
+
*
|
|
330
|
+
* **Why a registry distinct from {@link Exporter}**: an `Exporter`
|
|
331
|
+
* (io-types.ts) is download-oriented (`mediaType`/`extension`/`Blob`) and
|
|
332
|
+
* has no notion of per-format options. A code generator is preview-and-copy
|
|
333
|
+
* oriented and almost always carries a few knobs (component name, TS vs JS,
|
|
334
|
+
* encoding…). Modelling it as its own contract keeps the asset-export panel
|
|
335
|
+
* (binary/visual outputs) and the code-generation dialog (textual outputs)
|
|
336
|
+
* cleanly separated — exactly the split the user asked for: code is reviewed
|
|
337
|
+
* and copied, not configured-and-batched like a PNG slot.
|
|
338
|
+
*
|
|
339
|
+
* **Pure** (like `svgExporter`): `generate()` MUST be a pure
|
|
340
|
+
* `SvgDocument → string` transform — no DOM, no service access, no side
|
|
341
|
+
* effects. The "optimize first" step (running the {@link OptimizerRegistry}
|
|
342
|
+
* pipeline) happens BEFORE `generate()` is called, in the UI layer, so the
|
|
343
|
+
* generator always starts from a plain document. Worker-safe.
|
|
344
|
+
*
|
|
345
|
+
* **Plugin contribution**: generators register via
|
|
346
|
+
* `ctx.track(registry.register(gen))` exactly like importers/exporters —
|
|
347
|
+
* see {@link CodeGeneratorRegistry}.
|
|
348
|
+
*/
|
|
349
|
+
interface CodeGenerator {
|
|
350
|
+
/** Unique id within the {@link CodeGeneratorRegistry}. */
|
|
351
|
+
readonly id: string;
|
|
352
|
+
/** Human-readable label for the format dropdown (e.g. "React JSX"). */
|
|
353
|
+
readonly name: string;
|
|
354
|
+
/** Optional one-line description shown under the format picker. */
|
|
355
|
+
readonly description?: string;
|
|
356
|
+
/**
|
|
357
|
+
* Language hint for the output — drives the dialog subtitle and the
|
|
358
|
+
* Download filename extension fallback. e.g. `'tsx' | 'jsx' | 'text'`.
|
|
359
|
+
*/
|
|
360
|
+
readonly language: string;
|
|
361
|
+
/** File extension (no leading dot) for the Download action. */
|
|
362
|
+
readonly extension: string;
|
|
363
|
+
/**
|
|
364
|
+
* Declarative option specs the UI renders **generically** (no per-format
|
|
365
|
+
* hardcode). Omit for a zero-option generator.
|
|
366
|
+
*/
|
|
367
|
+
readonly options?: readonly CodeGeneratorOptionSpec[];
|
|
368
|
+
/**
|
|
369
|
+
* Transform the document into source code. `options` carries the resolved
|
|
370
|
+
* values for {@link options} (use {@link resolveCodeGeneratorOptionDefaults}
|
|
371
|
+
* to seed them); a generator MUST tolerate a missing/partial `options` by
|
|
372
|
+
* falling back to its declared defaults.
|
|
373
|
+
*/
|
|
374
|
+
generate(document: SvgDocument, options?: CodeGeneratorOptions): string;
|
|
375
|
+
}
|
|
376
|
+
/** Allowed value types for a generator option. */
|
|
377
|
+
type CodeGeneratorOptionValue = string | number | boolean;
|
|
378
|
+
/** A free-text option (e.g. the component name). */
|
|
379
|
+
interface CodeGeneratorTextOption {
|
|
380
|
+
readonly kind: 'text';
|
|
381
|
+
readonly key: string;
|
|
382
|
+
readonly label: string;
|
|
383
|
+
readonly default: string;
|
|
384
|
+
readonly placeholder?: string;
|
|
385
|
+
readonly hint?: string;
|
|
386
|
+
}
|
|
387
|
+
/** An on/off option (e.g. TypeScript, currentColor). */
|
|
388
|
+
interface CodeGeneratorBooleanOption {
|
|
389
|
+
readonly kind: 'boolean';
|
|
390
|
+
readonly key: string;
|
|
391
|
+
readonly label: string;
|
|
392
|
+
readonly default: boolean;
|
|
393
|
+
readonly hint?: string;
|
|
394
|
+
}
|
|
395
|
+
/** A pick-one option (e.g. Data URI encoding url/base64). */
|
|
396
|
+
interface CodeGeneratorSelectOption {
|
|
397
|
+
readonly kind: 'select';
|
|
398
|
+
readonly key: string;
|
|
399
|
+
readonly label: string;
|
|
400
|
+
readonly default: string;
|
|
401
|
+
readonly choices: readonly {
|
|
402
|
+
readonly value: string;
|
|
403
|
+
readonly label: string;
|
|
404
|
+
}[];
|
|
405
|
+
readonly hint?: string;
|
|
406
|
+
}
|
|
407
|
+
/** Discriminated union of every option control the dialog can render. */
|
|
408
|
+
type CodeGeneratorOptionSpec = CodeGeneratorTextOption | CodeGeneratorBooleanOption | CodeGeneratorSelectOption;
|
|
409
|
+
/** Resolved option values keyed by {@link CodeGeneratorOptionSpec.key}. */
|
|
410
|
+
type CodeGeneratorOptions = Record<string, CodeGeneratorOptionValue>;
|
|
411
|
+
/**
|
|
412
|
+
* Build the default option map for a generator (`{ key: default }` for every
|
|
413
|
+
* declared option). The UI seeds its editable option state from this; a
|
|
414
|
+
* generator called with no `options` behaves as if seeded from it too.
|
|
415
|
+
*/
|
|
416
|
+
declare function resolveCodeGeneratorOptionDefaults(gen: CodeGenerator): CodeGeneratorOptions;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* **D-110 — Registry of {@link CodeGenerator}s.** Signal-backed, same shape
|
|
420
|
+
* as {@link ExporterRegistry} / {@link ImporterRegistry}: `register()`
|
|
421
|
+
* returns a `Disposable`, lookups are insertion-ordered.
|
|
422
|
+
*
|
|
423
|
+
* Plugins contribute formats via `ctx.track(reg.register(generator))`; the
|
|
424
|
+
* built-in Group-A generators (React JSX / React Component / Data URI) are
|
|
425
|
+
* registered by `codeGeneratorsPlugin` (svg-engine/ui). Headless consumers
|
|
426
|
+
* can register the bare generators from `svg-engine/io` directly.
|
|
427
|
+
*
|
|
428
|
+
* `providedIn: 'root'` so the registry is a single shared instance — the
|
|
429
|
+
* code-generation dialog and any plugin see the same set.
|
|
430
|
+
*/
|
|
431
|
+
declare class CodeGeneratorRegistry {
|
|
432
|
+
private readonly _generators;
|
|
433
|
+
/** Reactive snapshot of all registered generators (insertion order). */
|
|
434
|
+
readonly generators: i0.Signal<readonly CodeGenerator[]>;
|
|
435
|
+
register(generator: CodeGenerator): Disposable;
|
|
436
|
+
get(id: string): CodeGenerator | null;
|
|
437
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<CodeGeneratorRegistry, never>;
|
|
438
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<CodeGeneratorRegistry>;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Drop the leading `<?xml …?>` declaration — unwanted in JSX / data URIs. */
|
|
442
|
+
declare function stripXmlProlog(svg: string): string;
|
|
443
|
+
/**
|
|
444
|
+
* Replace concrete `fill`/`stroke` colors with `currentColor` so the output
|
|
445
|
+
* inherits the surrounding text color (the standard themeable-icon pattern).
|
|
446
|
+
* Leaves `none`, `transparent`, `url(#…)` paint-server refs, and values
|
|
447
|
+
* already set to `currentColor` untouched.
|
|
448
|
+
*/
|
|
449
|
+
declare function applyCurrentColor(svg: string): string;
|
|
450
|
+
/**
|
|
451
|
+
* Rewrite a serialized SVG string into JSX: strips the `<?xml?>` prolog and
|
|
452
|
+
* rewrites every opening/self-closing tag's attributes (names → JSX, `style`
|
|
453
|
+
* string → object). Text nodes and entities are left as-is — JSX decodes the
|
|
454
|
+
* exporter's `&`/`<`/`>` in element children correctly.
|
|
455
|
+
*/
|
|
456
|
+
declare function svgStringToJsx(svg: string): string;
|
|
457
|
+
/**
|
|
458
|
+
* Serialize an SVG string to a `data:` URI. `url` encoding (default) uses
|
|
459
|
+
* `encodeURIComponent` — valid in `<img src>` and (quoted) CSS `url()`, and
|
|
460
|
+
* typically smaller than base64 for attribute-light icons. `base64` is the
|
|
461
|
+
* classic, universally-accepted form.
|
|
462
|
+
*/
|
|
463
|
+
declare function svgToDataUri(svg: string, encoding: 'url' | 'base64'): string;
|
|
464
|
+
/** Coerce an arbitrary label into a PascalCase identifier (≥1 leading letter). */
|
|
465
|
+
declare function toPascalCaseComponentName(name: string): string;
|
|
466
|
+
/** Inline SVG-as-JSX (camelCased attributes) for pasting into a component. */
|
|
467
|
+
declare const reactJsxGenerator: CodeGenerator;
|
|
468
|
+
/** A standalone React component (.tsx/.jsx) that spreads props onto `<svg>`. */
|
|
469
|
+
declare const reactComponentGenerator: CodeGenerator;
|
|
470
|
+
/** A `data:image/svg+xml,…` URI for CSS `background-image` or an `<img>` src. */
|
|
471
|
+
declare const dataUriGenerator: CodeGenerator;
|
|
472
|
+
/** The Group-A built-in set, in display order. */
|
|
473
|
+
declare const BUILTIN_CODE_GENERATORS: readonly CodeGenerator[];
|
|
474
|
+
|
|
475
|
+
export { BUILTIN_CODE_GENERATORS, CodeGeneratorRegistry, ExporterRegistry, ImporterRegistry, applyCurrentColor, collectDefsIds, dataUriGenerator, gunzipText, gzipText, mergeDefsFragments, namespaceCollidingDefs, nodeToSvgMarkup, pngExporter, reactComponentGenerator, reactJsxGenerator, renderPng, resolveCodeGeneratorOptionDefaults, stripXmlProlog, svgExporter, svgImporter, svgStringToJsx, svgToDataUri, svgzExporter, toPascalCaseComponentName };
|
|
476
|
+
export type { CodeGenerator, CodeGeneratorBooleanOption, CodeGeneratorOptionSpec, CodeGeneratorOptionValue, CodeGeneratorOptions, CodeGeneratorSelectOption, CodeGeneratorTextOption, Exporter, ImportResult, Importer, NamespacedDefs };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { SvgDocument, Disposable, Command, CommandContext, CommandResult } from '@mosaicoo/svg-engine/core';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin contribution to the SVG optimization pipeline (categoria 3
|
|
6
|
+
* do D-023). Pure transformation `SvgDocument → SvgDocument`.
|
|
7
|
+
*
|
|
8
|
+
* **Contract**:
|
|
9
|
+
* - `optimize(doc)` MUST be pure (no DOM, no service access, no
|
|
10
|
+
* side effects). Worker-safe.
|
|
11
|
+
* - MUST return a structurally-equivalent document on no-op input
|
|
12
|
+
* (lets the pipeline detect "nothing changed" via reference
|
|
13
|
+
* equality and short-circuit).
|
|
14
|
+
* - SHOULD NOT mutate the input — return a new document via
|
|
15
|
+
* `{ ...doc, root: ... }` when changes are needed.
|
|
16
|
+
*
|
|
17
|
+
* **Fields**:
|
|
18
|
+
* - `id`: unique within {@link OptimizerRegistry}
|
|
19
|
+
* - `name`: human-readable label for UIs
|
|
20
|
+
* - `description`: optional one-liner explaining what the pass does
|
|
21
|
+
* - `order`: sort key in the pipeline (lower runs first). Default
|
|
22
|
+
* 100. Use lower for "structural" passes that other passes can
|
|
23
|
+
* then optimize on top of (e.g., precision-round before dedupe).
|
|
24
|
+
* - `defaultEnabled`: whether the pipeline includes this pass when
|
|
25
|
+
* no explicit selection is provided. Default `true`.
|
|
26
|
+
*/
|
|
27
|
+
interface Optimizer {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly description?: string;
|
|
31
|
+
readonly order?: number;
|
|
32
|
+
readonly defaultEnabled?: boolean;
|
|
33
|
+
optimize(document: SvgDocument): SvgDocument;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Registry of {@link Optimizer}s (categoria 3 do D-023). Signal-backed,
|
|
38
|
+
* `register()` returns `Disposable`. Plugins contribute passes via
|
|
39
|
+
* `ctx.track(reg.register(opt))`.
|
|
40
|
+
*
|
|
41
|
+
* The registry also offers a {@link runPipeline} convenience that
|
|
42
|
+
* sorts active optimizers by `order` (default 100, stable sort on
|
|
43
|
+
* ties) and chains them — most consumers want this, not raw enumeration.
|
|
44
|
+
*/
|
|
45
|
+
declare class OptimizerRegistry {
|
|
46
|
+
private readonly _optimizers;
|
|
47
|
+
/** Reactive snapshot (insertion order; pipeline reorders by `order`). */
|
|
48
|
+
readonly optimizers: i0.Signal<readonly Optimizer[]>;
|
|
49
|
+
register(optimizer: Optimizer): Disposable;
|
|
50
|
+
get(id: string): Optimizer | null;
|
|
51
|
+
/**
|
|
52
|
+
* Run all enabled optimizers in `order` ascending. When
|
|
53
|
+
* `enabledIds` is provided, only those (intersected with available)
|
|
54
|
+
* run — useful for "advanced settings" UIs where users toggle
|
|
55
|
+
* individual passes. When omitted, all optimizers with
|
|
56
|
+
* `defaultEnabled !== false` run.
|
|
57
|
+
*
|
|
58
|
+
* Returns a new document with all passes applied in order. If no
|
|
59
|
+
* pass changes the document (reference equality), the original
|
|
60
|
+
* document is returned unchanged.
|
|
61
|
+
*/
|
|
62
|
+
runPipeline(document: SvgDocument, enabledIds?: ReadonlySet<string> | null): SvgDocument;
|
|
63
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<OptimizerRegistry, never>;
|
|
64
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<OptimizerRegistry>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Round all geometric numeric values to a fixed precision (default 3
|
|
69
|
+
* decimal places). Eliminates `0.30000000000000004` and friends that
|
|
70
|
+
* accumulate from floating-point drag / scale gestures, without losing
|
|
71
|
+
* meaningful coordinate precision.
|
|
72
|
+
*
|
|
73
|
+
* **Touches**: rect/ellipse/line/polygon/polyline/text/image positions
|
|
74
|
+
* + sizes + radii; path `d` numeric tokens (via regex); transform
|
|
75
|
+
* matrix components; style strokeWidth/opacity/fillOpacity/strokeOpacity.
|
|
76
|
+
*
|
|
77
|
+
* **Doesn't touch**: ids, hrefs, colors, transform STRUCTURE (only the
|
|
78
|
+
* numbers inside it).
|
|
79
|
+
*
|
|
80
|
+
* **Order 10** — runs FIRST in the default pipeline so later passes
|
|
81
|
+
* see clean rounded values.
|
|
82
|
+
*/
|
|
83
|
+
declare const precisionOptimizer: Optimizer;
|
|
84
|
+
/**
|
|
85
|
+
* Strip presentation-attribute values that match the SVG default
|
|
86
|
+
* (e.g., `fill-opacity="1"`, `stroke-width="1"`). These attributes
|
|
87
|
+
* are redundant in the output — removing them shrinks the file and
|
|
88
|
+
* makes diffs less noisy.
|
|
89
|
+
*
|
|
90
|
+
* **Defaults handled**:
|
|
91
|
+
* - `fillOpacity === 1`
|
|
92
|
+
* - `strokeOpacity === 1`
|
|
93
|
+
* - `opacity === 1`
|
|
94
|
+
* - `visibility === 'visible'`
|
|
95
|
+
*
|
|
96
|
+
* **Order 50** — runs in the middle of the pipeline so precision
|
|
97
|
+
* rounding (10) already cleaned the values + later passes (e.g.,
|
|
98
|
+
* group pruning) see the minimized style.
|
|
99
|
+
*
|
|
100
|
+
* **NOT handled**: `fill === 'black'` / `stroke === 'none'` etc. Those
|
|
101
|
+
* are arguably "defaults" but stripping them changes rendered output
|
|
102
|
+
* if the parent specifies a different value (CSS inheritance). Safer
|
|
103
|
+
* to leave color defaults explicit.
|
|
104
|
+
*/
|
|
105
|
+
declare const dropDefaultsOptimizer: Optimizer;
|
|
106
|
+
/**
|
|
107
|
+
* **D-072 follow-up — Strip authored-title emission preference.**
|
|
108
|
+
*
|
|
109
|
+
* Sets `document.exportPreferences.emitAuthoredTitles = false` so the
|
|
110
|
+
* SVG exporter omits the `<title>...</title>` child element it would
|
|
111
|
+
* otherwise emit for every node with `metadata.name`.
|
|
112
|
+
*
|
|
113
|
+
* **What it does NOT do**: remove `metadata.name` itself. The name
|
|
114
|
+
* stays on the model — the layer panel still shows "Bercos", a future
|
|
115
|
+
* export with the preference un-stripped (or the in-editor session)
|
|
116
|
+
* still has the name available. Stripping is purely an export-side
|
|
117
|
+
* decision, reversible by toggling the preference back to `true`.
|
|
118
|
+
*
|
|
119
|
+
* **Order 80** — runs after value-rounding (10) and default-dropping
|
|
120
|
+
* (50), before group-pruning (90). Position doesn't really matter for
|
|
121
|
+
* this pass (it doesn't touch the tree), but keeping it in the middle
|
|
122
|
+
* makes the pipeline read top-to-bottom by "what touches what".
|
|
123
|
+
*
|
|
124
|
+
* **Opt-in** (`defaultEnabled: false`): the default pipeline preserves
|
|
125
|
+
* authored names — stripping is for minified/production-ready output
|
|
126
|
+
* where editor metadata is unwanted.
|
|
127
|
+
*/
|
|
128
|
+
declare const stripAuthoredTitlesOptimizer: Optimizer;
|
|
129
|
+
/**
|
|
130
|
+
* Remove empty groups (`<g>` with no children) recursively. Common
|
|
131
|
+
* after a delete-then-undo cycle or after the user ungroups+regroups.
|
|
132
|
+
*
|
|
133
|
+
* Preserves the document root (which is always a group) even if it
|
|
134
|
+
* becomes empty — the root is a structural anchor, not a layer.
|
|
135
|
+
*
|
|
136
|
+
* **Order 90** — runs after value-rounding (10) and default-stripping
|
|
137
|
+
* (50) so groups that became "empty" via earlier passes are picked up
|
|
138
|
+
* in the same pipeline run.
|
|
139
|
+
*/
|
|
140
|
+
declare const pruneEmptyGroupsOptimizer: Optimizer;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run the optimization pipeline as ONE undoable command (Item 3 —
|
|
144
|
+
* débito 5-Optimize).
|
|
145
|
+
*
|
|
146
|
+
* Pre-Item-3, the playground called `OptimizerRegistry.runPipeline()`
|
|
147
|
+
* directly and bypassed the undo stack — Optimize was destructive
|
|
148
|
+
* to history because the user couldn't `Ctrl+Z` back to the pre-
|
|
149
|
+
* optimize state. Now wrapping the pipeline in a Command produces
|
|
150
|
+
* exactly one undo entry covering the whole pass set.
|
|
151
|
+
*
|
|
152
|
+
* **Construction-time dependency injection of the registry**: the
|
|
153
|
+
* command needs access to the live `OptimizerRegistry`. Since
|
|
154
|
+
* `Command` doesn't have an injector (its `CommandContext` only
|
|
155
|
+
* exposes `state`), we pass the registry to the constructor. The
|
|
156
|
+
* playground / consumer injects `OptimizerRegistry` and passes it
|
|
157
|
+
* in.
|
|
158
|
+
*
|
|
159
|
+
* **No-op when pipeline doesn't change the document**: the registry's
|
|
160
|
+
* `runPipeline` returns the same reference when no pass mutated;
|
|
161
|
+
* we detect that and return `ok()` without pushing onto history.
|
|
162
|
+
*
|
|
163
|
+
* **Undo**: restores the pre-optimize document via the captured
|
|
164
|
+
* snapshot (one reference; structural sharing means it's cheap).
|
|
165
|
+
*
|
|
166
|
+
* **`enabledIds`**: forwarded to `runPipeline` so the caller can
|
|
167
|
+
* pick a subset of passes (e.g., from an "advanced settings" UI).
|
|
168
|
+
* When omitted, all `defaultEnabled` passes run.
|
|
169
|
+
*/
|
|
170
|
+
declare class OptimizeCommand implements Command {
|
|
171
|
+
private readonly registry;
|
|
172
|
+
private readonly enabledIds?;
|
|
173
|
+
readonly id: string;
|
|
174
|
+
readonly label = "Optimize";
|
|
175
|
+
readonly isDestructive = true;
|
|
176
|
+
private previousDocument;
|
|
177
|
+
constructor(registry: OptimizerRegistry, enabledIds?: (ReadonlySet<string> | null) | undefined);
|
|
178
|
+
execute(ctx: CommandContext): CommandResult;
|
|
179
|
+
undo(ctx: CommandContext): CommandResult;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { OptimizeCommand, OptimizerRegistry, dropDefaultsOptimizer, precisionOptimizer, pruneEmptyGroupsOptimizer, stripAuthoredTitlesOptimizer };
|
|
183
|
+
export type { Optimizer };
|