@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.
@@ -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 `&amp;`/`&lt;`/`&gt;` 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 };