@1agh/maude 0.24.0 → 0.26.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/cli/commands/design.mjs +5 -0
- package/cli/lib/design-link.mjs +13 -6
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/gitignore-block.test.mjs +1 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
- package/plugins/design/dev-server/annotations-layer.tsx +817 -170
- package/plugins/design/dev-server/api.ts +15 -1
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-optimize.mjs +35 -0
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/draw-build.sh +48 -0
- package/plugins/design/dev-server/bin/draw-proof.sh +129 -0
- package/plugins/design/dev-server/bin/smoke.sh +114 -12
- package/plugins/design/dev-server/bin/svg-optimize.sh +24 -0
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +130 -82
- package/plugins/design/dev-server/canvas-icons.tsx +169 -0
- package/plugins/design/dev-server/canvas-lib.tsx +110 -0
- package/plugins/design/dev-server/canvas-shell.tsx +113 -89
- package/plugins/design/dev-server/client/app.jsx +1084 -417
- package/plugins/design/dev-server/config.schema.json +10 -0
- package/plugins/design/dev-server/context.ts +9 -0
- package/plugins/design/dev-server/dist/client.bundle.js +242 -20
- package/plugins/design/dev-server/dist/comment-mount.js +40 -62
- package/plugins/design/dev-server/draw/brush.ts +639 -0
- package/plugins/design/dev-server/draw/composition.ts +229 -0
- package/plugins/design/dev-server/draw/geometry.ts +578 -0
- package/plugins/design/dev-server/draw/index.ts +28 -0
- package/plugins/design/dev-server/draw/layout.ts +260 -0
- package/plugins/design/dev-server/draw/optimize.ts +65 -0
- package/plugins/design/dev-server/draw/palette.ts +417 -0
- package/plugins/design/dev-server/draw/primitives.ts +643 -0
- package/plugins/design/dev-server/draw/serialize.ts +458 -0
- package/plugins/design/dev-server/draw/test/brush.test.ts +213 -0
- package/plugins/design/dev-server/draw/test/composition.test.ts +141 -0
- package/plugins/design/dev-server/draw/test/geometry.test.ts +199 -0
- package/plugins/design/dev-server/draw/test/gradient.test.ts +167 -0
- package/plugins/design/dev-server/draw/test/layout.test.ts +120 -0
- package/plugins/design/dev-server/draw/test/optimize.test.ts +40 -0
- package/plugins/design/dev-server/draw/test/palette.test.ts +123 -0
- package/plugins/design/dev-server/draw/test/primitives.test.ts +129 -0
- package/plugins/design/dev-server/draw/test/serialize.test.ts +105 -0
- package/plugins/design/dev-server/export-dialog.tsx +189 -1
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/http.ts +40 -9
- package/plugins/design/dev-server/input-router.tsx +9 -2
- package/plugins/design/dev-server/sync/index.ts +73 -17
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +11 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +52 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
- package/plugins/design/dev-server/tool-palette.tsx +140 -11
- package/plugins/design/dev-server/tsconfig.json +8 -1
- package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
- package/plugins/design/templates/_shell.html +36 -9
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +15 -0
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file draw/layout.ts — Phase 25 geometry-engine layout / diagram layer
|
|
3
|
+
* @scope plugins/design/dev-server/draw/layout.ts
|
|
4
|
+
* @purpose Composition math:
|
|
5
|
+
* • a deterministic constraint-based label placer (pick the
|
|
6
|
+
* candidate slot around each anchor that minimizes overlap —
|
|
7
|
+
* NO `Math.random`, so output is reproducible + testable);
|
|
8
|
+
* • 4/8pt grid snapping + modular type-scale ladders (the
|
|
9
|
+
* spacing/type rubric checks 5 + 9);
|
|
10
|
+
* • `diagram()` — composes nodes + A*-routed edges + labels into
|
|
11
|
+
* primitives, the thing the full engine unlocks that LLM
|
|
12
|
+
* free-handing can't do (edges that never cross nodes).
|
|
13
|
+
*
|
|
14
|
+
* React-free (DDR-067).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type Rect, routeConnector } from './geometry.ts';
|
|
18
|
+
import { type DrawPrimitive, type Point, group, polyline, rect, snap, text } from './primitives.ts';
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Grid + modular scale
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Snap a point to a grid base (4 / 8 pt). `base <= 0` is a no-op. */
|
|
25
|
+
export function snapToGrid(p: Point, base: number): Point {
|
|
26
|
+
return { x: snap(p.x, base), y: snap(p.y, base) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A modular type/space scale: `base · ratio^(startStep + i)` for `count` steps.
|
|
31
|
+
* e.g. `modularScale(16, 1.5, 3)` → `[16, 24, 36]`. Common ratios: 1.2 (minor
|
|
32
|
+
* third), 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth).
|
|
33
|
+
*/
|
|
34
|
+
export function modularScale(base: number, ratio: number, count: number, startStep = 0): number[] {
|
|
35
|
+
const out: number[] = [];
|
|
36
|
+
for (let i = 0; i < count; i++) {
|
|
37
|
+
out.push(Math.round(base * ratio ** (startStep + i) * 1000) / 1000);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Constraint-based label placement
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export interface LabelItem {
|
|
47
|
+
id: string;
|
|
48
|
+
anchor: Point;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Placement {
|
|
54
|
+
id: string;
|
|
55
|
+
/** Top-left of the placed label box. */
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
/** Which compass slot was chosen. */
|
|
61
|
+
slot: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PlaceLabelsOpts {
|
|
65
|
+
/** Gap between the anchor and the nearest label edge (default 6). */
|
|
66
|
+
gap?: number;
|
|
67
|
+
/** Optional bounds the labels must stay inside. */
|
|
68
|
+
bounds?: Rect;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Candidate slots in deterministic priority order (E reads best, then below…).
|
|
72
|
+
const SLOTS = ['E', 'S', 'W', 'N', 'SE', 'NE', 'SW', 'NW'] as const;
|
|
73
|
+
|
|
74
|
+
function slotBox(item: LabelItem, slot: string, gap: number): Rect {
|
|
75
|
+
const { anchor: a, width: w, height: h } = item;
|
|
76
|
+
switch (slot) {
|
|
77
|
+
case 'E':
|
|
78
|
+
return { x: a.x + gap, y: a.y - h / 2, width: w, height: h };
|
|
79
|
+
case 'W':
|
|
80
|
+
return { x: a.x - gap - w, y: a.y - h / 2, width: w, height: h };
|
|
81
|
+
case 'S':
|
|
82
|
+
return { x: a.x - w / 2, y: a.y + gap, width: w, height: h };
|
|
83
|
+
case 'N':
|
|
84
|
+
return { x: a.x - w / 2, y: a.y - gap - h, width: w, height: h };
|
|
85
|
+
case 'SE':
|
|
86
|
+
return { x: a.x + gap, y: a.y + gap, width: w, height: h };
|
|
87
|
+
case 'NE':
|
|
88
|
+
return { x: a.x + gap, y: a.y - gap - h, width: w, height: h };
|
|
89
|
+
case 'SW':
|
|
90
|
+
return { x: a.x - gap - w, y: a.y + gap, width: w, height: h };
|
|
91
|
+
default: // NW
|
|
92
|
+
return { x: a.x - gap - w, y: a.y - gap - h, width: w, height: h };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function overlapArea(a: Rect, b: Rect): number {
|
|
97
|
+
const ox = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
|
|
98
|
+
const oy = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
|
99
|
+
return ox * oy;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function outOfBounds(box: Rect, bounds: Rect): number {
|
|
103
|
+
const dx =
|
|
104
|
+
Math.max(0, bounds.x - box.x) + Math.max(0, box.x + box.width - (bounds.x + bounds.width));
|
|
105
|
+
const dy =
|
|
106
|
+
Math.max(0, bounds.y - box.y) + Math.max(0, box.y + box.height - (bounds.y + bounds.height));
|
|
107
|
+
return dx + dy;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Place each label in the slot around its anchor that minimizes overlap with
|
|
112
|
+
* already-placed labels, the other anchors, and the bounds. Greedy + ordered by
|
|
113
|
+
* id, so the result is fully deterministic. Returns one {@link Placement} per
|
|
114
|
+
* item, in input order.
|
|
115
|
+
*/
|
|
116
|
+
export function placeLabels(items: LabelItem[], opts: PlaceLabelsOpts = {}): Placement[] {
|
|
117
|
+
const gap = opts.gap ?? 6;
|
|
118
|
+
const order = [...items].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
119
|
+
const placed: Rect[] = [];
|
|
120
|
+
const anchors = items.map((it) => it.anchor);
|
|
121
|
+
const byId = new Map<string, Placement>();
|
|
122
|
+
|
|
123
|
+
for (const item of order) {
|
|
124
|
+
let best: { slot: string; box: Rect; penalty: number } | null = null;
|
|
125
|
+
for (const slot of SLOTS) {
|
|
126
|
+
const box = slotBox(item, slot, gap);
|
|
127
|
+
let penalty = 0;
|
|
128
|
+
for (const p of placed) penalty += overlapArea(box, p);
|
|
129
|
+
// Don't sit on top of another anchor point.
|
|
130
|
+
for (const a of anchors) {
|
|
131
|
+
if (a === item.anchor) continue;
|
|
132
|
+
if (a.x >= box.x && a.x <= box.x + box.width && a.y >= box.y && a.y <= box.y + box.height) {
|
|
133
|
+
penalty += gap * gap;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (opts.bounds) penalty += outOfBounds(box, opts.bounds) * 1000;
|
|
137
|
+
if (best === null || penalty < best.penalty) best = { slot, box, penalty };
|
|
138
|
+
if (penalty === 0) break; // perfect slot — take it (priority order honored)
|
|
139
|
+
}
|
|
140
|
+
const b = best as { slot: string; box: Rect };
|
|
141
|
+
placed.push(b.box);
|
|
142
|
+
byId.set(item.id, {
|
|
143
|
+
id: item.id,
|
|
144
|
+
x: b.box.x,
|
|
145
|
+
y: b.box.y,
|
|
146
|
+
width: item.width,
|
|
147
|
+
height: item.height,
|
|
148
|
+
slot: b.slot,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return items.map((it) => byId.get(it.id) as Placement);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
// diagram() — nodes + A* edges + labels
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export interface DiagramNode {
|
|
160
|
+
id: string;
|
|
161
|
+
rect: Rect;
|
|
162
|
+
label?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface DiagramEdge {
|
|
166
|
+
from: string;
|
|
167
|
+
to: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface DiagramOpts {
|
|
171
|
+
grid?: number;
|
|
172
|
+
/** Inflate nodes when routing so edges keep clearance (default 4). */
|
|
173
|
+
padding?: number;
|
|
174
|
+
/** Corner chamfer radius for the routed edges (default 4). */
|
|
175
|
+
chamfer?: number;
|
|
176
|
+
nodeStyle?: { fill?: string; stroke?: string; strokeWidth?: number; rx?: number };
|
|
177
|
+
edgeStyle?: { stroke?: string; strokeWidth?: number };
|
|
178
|
+
labelStyle?: { fontSize?: number; fontFamily?: string; fontWeight?: number };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function center(r: Rect): Point {
|
|
182
|
+
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compose a node-link diagram into ordered primitives: edges first (so they
|
|
187
|
+
* tuck behind nodes), then each node's rect + centered label. Edges are routed
|
|
188
|
+
* with the A* connector around every OTHER node, so they never cross a box.
|
|
189
|
+
*/
|
|
190
|
+
export function diagram(
|
|
191
|
+
nodes: DiagramNode[],
|
|
192
|
+
edges: DiagramEdge[],
|
|
193
|
+
opts: DiagramOpts = {}
|
|
194
|
+
): DrawPrimitive[] {
|
|
195
|
+
const grid = opts.grid ?? 10;
|
|
196
|
+
const padding = opts.padding ?? 4;
|
|
197
|
+
const chamfer = opts.chamfer ?? 4;
|
|
198
|
+
const ns = opts.nodeStyle ?? {};
|
|
199
|
+
const es = opts.edgeStyle ?? {};
|
|
200
|
+
const ls = opts.labelStyle ?? {};
|
|
201
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
202
|
+
|
|
203
|
+
const edgePrims: DrawPrimitive[] = [];
|
|
204
|
+
for (const e of edges) {
|
|
205
|
+
const a = byId.get(e.from);
|
|
206
|
+
const b = byId.get(e.to);
|
|
207
|
+
if (!a || !b) continue;
|
|
208
|
+
const obstacles = nodes.filter((n) => n.id !== e.from && n.id !== e.to).map((n) => n.rect);
|
|
209
|
+
const points = routeConnector(center(a.rect), center(b.rect), obstacles, {
|
|
210
|
+
grid,
|
|
211
|
+
padding,
|
|
212
|
+
chamfer,
|
|
213
|
+
});
|
|
214
|
+
edgePrims.push(
|
|
215
|
+
polyline({
|
|
216
|
+
points,
|
|
217
|
+
fill: 'none',
|
|
218
|
+
stroke: es.stroke ?? 'currentColor',
|
|
219
|
+
strokeWidth: es.strokeWidth ?? 1.5,
|
|
220
|
+
strokeLinejoin: 'round',
|
|
221
|
+
strokeLinecap: 'round',
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const nodePrims: DrawPrimitive[] = [];
|
|
227
|
+
for (const n of nodes) {
|
|
228
|
+
const r = n.rect;
|
|
229
|
+
nodePrims.push(
|
|
230
|
+
rect({
|
|
231
|
+
x: r.x,
|
|
232
|
+
y: r.y,
|
|
233
|
+
width: r.width,
|
|
234
|
+
height: r.height,
|
|
235
|
+
rx: ns.rx ?? 6,
|
|
236
|
+
fill: ns.fill ?? 'none',
|
|
237
|
+
stroke: ns.stroke ?? 'currentColor',
|
|
238
|
+
strokeWidth: ns.strokeWidth ?? 1.5,
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
if (n.label) {
|
|
242
|
+
const c = center(r);
|
|
243
|
+
nodePrims.push(
|
|
244
|
+
text({
|
|
245
|
+
x: c.x,
|
|
246
|
+
y: c.y,
|
|
247
|
+
content: n.label,
|
|
248
|
+
textAnchor: 'middle',
|
|
249
|
+
dominantBaseline: 'central',
|
|
250
|
+
fontSize: ls.fontSize ?? 13,
|
|
251
|
+
fontFamily: ls.fontFamily,
|
|
252
|
+
fontWeight: ls.fontWeight,
|
|
253
|
+
fill: 'currentColor',
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return [group(edgePrims, { id: 'edges' }), group(nodePrims, { id: 'nodes' })];
|
|
260
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file draw/optimize.ts — Phase 25 geometry-engine SVGO wrapper
|
|
3
|
+
* @scope plugins/design/dev-server/draw/optimize.ts
|
|
4
|
+
* @purpose The single external dependency (SVGO — DDR-071). Two jobs:
|
|
5
|
+
* 1. Minify the final on-disk SVG asset (multipass, 2-decimal
|
|
6
|
+
* precision) without stripping the a11y/scaling contract.
|
|
7
|
+
* 2. Act as a parse/validity GATE — SVGO's parser throws on
|
|
8
|
+
* malformed SVG, so a mangled engine output (or hand-edited
|
|
9
|
+
* asset) is caught loudly instead of shipping a broken mark.
|
|
10
|
+
*
|
|
11
|
+
* SVGO v4 note (verified against svgo@4.0.1): `preset-default` no
|
|
12
|
+
* longer includes `removeViewBox` / `removeDesc`, so viewBox, the
|
|
13
|
+
* `<title>` and the `<desc>` are PRESERVED by default — the
|
|
14
|
+
* accessibility + responsive-scaling contract survives optimization
|
|
15
|
+
* with no overrides needed. Passing `removeViewBox:false` as an
|
|
16
|
+
* override actually warns ("not part of preset-default"), so we
|
|
17
|
+
* deliberately don't. If a future SVGO bump reintroduces those
|
|
18
|
+
* plugins, re-add explicit `false` overrides here.
|
|
19
|
+
*
|
|
20
|
+
* This is the ONE engine module with a runtime dependency; the rest
|
|
21
|
+
* (primitives/geometry/palette/serialize/layout) are pure + dep-free.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { optimize as svgoOptimize } from 'svgo';
|
|
25
|
+
|
|
26
|
+
export interface OptimizeOpts {
|
|
27
|
+
/** Coordinate decimal places (default 2). */
|
|
28
|
+
floatPrecision?: number;
|
|
29
|
+
/** Run the plugin pipeline repeatedly until stable (default true). */
|
|
30
|
+
multipass?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Optimize an SVG string. Keeps `viewBox`, `<title>`, `<desc>`. Throws a clear
|
|
35
|
+
* Error if the input doesn't parse (the validity gate).
|
|
36
|
+
*/
|
|
37
|
+
export function optimizeSvg(svg: string, opts: OptimizeOpts = {}): string {
|
|
38
|
+
const { floatPrecision = 2, multipass = true } = opts;
|
|
39
|
+
let result: { data: string };
|
|
40
|
+
try {
|
|
41
|
+
result = svgoOptimize(svg, {
|
|
42
|
+
multipass,
|
|
43
|
+
floatPrecision,
|
|
44
|
+
// Default plugin set = `preset-default`; in v4 that keeps viewBox/title/desc.
|
|
45
|
+
plugins: ['preset-default'],
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
throw new Error(`optimizeSvg: input is not valid SVG — ${msg}`);
|
|
50
|
+
}
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* True when `svg` parses as valid SVG. Wraps {@link optimizeSvg}'s validity
|
|
56
|
+
* gate without throwing — for callers that want a boolean pre-check.
|
|
57
|
+
*/
|
|
58
|
+
export function isValidSvg(svg: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
optimizeSvg(svg, { multipass: false });
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|