@grida/svg-editor 1.0.0-alpha.15 → 1.0.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/{dom-CK6GlgFF.d.mts → dom-98AUOfsP.d.mts} +44 -2
- package/dist/{dom-CsKXTaNw.d.ts → dom-BO2-E9oK.d.ts} +44 -2
- package/dist/{dom-DILY80j7.mjs → dom-DOvcMvl4.mjs} +67 -2
- package/dist/{dom-Dee6FtgZ.js → dom-U6ae5fQF.js} +72 -1
- package/dist/dom.d.mts +3 -3
- package/dist/dom.d.ts +3 -3
- package/dist/dom.js +2 -1
- package/dist/dom.mjs +2 -2
- package/dist/{editor-F8ckj9X1.js → editor-C6Lj1In-.js} +416 -865
- package/dist/{editor-BKoo9SPL.d.ts → editor-CYoGJ3Hf.d.ts} +311 -5
- package/dist/{editor-Dl7c0q5A.d.mts → editor-D2eQe8lB.d.mts} +311 -5
- package/dist/{editor-CvWpD5mu.mjs → editor-DKQOIKuU.mjs} +415 -865
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{model-CJ1Ctq14.js → model-D0nU_EkL.js} +1176 -62
- package/dist/{model-B2UWgViT.mjs → model-L3t9ixT_.mjs} +1171 -63
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +2 -2
- package/dist/presets.mjs +1 -1
- package/dist/react.d.mts +20 -3
- package/dist/react.d.ts +20 -3
- package/dist/react.js +25 -2
- package/dist/react.mjs +25 -3
- package/package.json +8 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import cmath from "@grida/cmath";
|
|
2
|
+
import { SVG_NS, XLINK_NS as XLINK_NS$1, XMLNS_NS, encode_attr_value, encode_text, parse_svg } from "@grida/svg/parser";
|
|
2
3
|
import { svg_parse } from "@grida/svg/parse";
|
|
3
4
|
import { SVGPathData, SVGPathDataTransformer, encodeSVGPath } from "@grida/svg/pathdata";
|
|
4
5
|
import vn from "@grida/vn";
|
|
@@ -26,75 +27,983 @@ function array_shallow_equal(a, b) {
|
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
//#endregion
|
|
29
|
-
//#region src/core/
|
|
30
|
-
|
|
31
|
-
(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
30
|
+
//#region src/core/document.ts
|
|
31
|
+
/** The native vector tags `retype_to_path` can re-type, keyed by tag → the
|
|
32
|
+
* native geometry attributes it consumes (so no orphaned geometry attr
|
|
33
|
+
* survives on the resulting `<path>`). Covers the geometry primitives
|
|
34
|
+
* (rect / circle / ellipse — always re-typed) and the vertex tags (line /
|
|
35
|
+
* polyline / polygon — re-typed only when an edit escapes their native
|
|
36
|
+
* form). */
|
|
37
|
+
const RETYPABLE_GEOMETRY_ATTRS = {
|
|
38
|
+
line: new Set([
|
|
39
|
+
"x1",
|
|
40
|
+
"y1",
|
|
41
|
+
"x2",
|
|
42
|
+
"y2"
|
|
43
|
+
]),
|
|
44
|
+
polyline: new Set(["points"]),
|
|
45
|
+
polygon: new Set(["points"]),
|
|
46
|
+
rect: new Set([
|
|
47
|
+
"x",
|
|
48
|
+
"y",
|
|
49
|
+
"width",
|
|
50
|
+
"height",
|
|
51
|
+
"rx",
|
|
52
|
+
"ry"
|
|
53
|
+
]),
|
|
54
|
+
circle: new Set([
|
|
55
|
+
"cx",
|
|
56
|
+
"cy",
|
|
57
|
+
"r"
|
|
58
|
+
]),
|
|
59
|
+
ellipse: new Set([
|
|
60
|
+
"cx",
|
|
61
|
+
"cy",
|
|
62
|
+
"rx",
|
|
63
|
+
"ry"
|
|
64
|
+
])
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Parse a single SVG length attribute as a plain user-unit number. Returns
|
|
68
|
+
* `null` for absent, non-finite, or unit/percentage values (`50%`, `5px`,
|
|
69
|
+
* `5em`) — those are an out-of-scope geometry gap, and refusing them here
|
|
70
|
+
* means the editor never offers a promotion it cannot perform faithfully.
|
|
71
|
+
*/
|
|
72
|
+
function parse_user_unit(raw) {
|
|
73
|
+
if (raw === null) return null;
|
|
74
|
+
const s = raw.trim();
|
|
75
|
+
if (s === "") return null;
|
|
76
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return null;
|
|
77
|
+
const n = Number(s);
|
|
78
|
+
return Number.isFinite(n) ? n : null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Attribute names whose writes can shift a node's rendered bounds.
|
|
82
|
+
* Membership drives `_geometry_version` bumps in `set_attr`. Only
|
|
83
|
+
* non-namespaced attribute names — namespaced writes (xlink:href, etc.)
|
|
84
|
+
* never bump because they're references, not geometry.
|
|
85
|
+
*
|
|
86
|
+
* Includes text-shaping attributes (font-*) because they re-shape glyph
|
|
87
|
+
* runs and change `<text>` bbox.
|
|
88
|
+
*/
|
|
89
|
+
const GEOMETRY_ATTRS = new Set([
|
|
90
|
+
"x",
|
|
91
|
+
"y",
|
|
92
|
+
"x1",
|
|
93
|
+
"y1",
|
|
94
|
+
"x2",
|
|
95
|
+
"y2",
|
|
96
|
+
"cx",
|
|
97
|
+
"cy",
|
|
98
|
+
"width",
|
|
99
|
+
"height",
|
|
100
|
+
"r",
|
|
101
|
+
"rx",
|
|
102
|
+
"ry",
|
|
103
|
+
"points",
|
|
104
|
+
"d",
|
|
105
|
+
"transform",
|
|
106
|
+
"viewBox",
|
|
107
|
+
"font-size",
|
|
108
|
+
"font-family",
|
|
109
|
+
"font-weight",
|
|
110
|
+
"font-style",
|
|
111
|
+
"text-anchor",
|
|
112
|
+
"dx",
|
|
113
|
+
"dy",
|
|
114
|
+
"rotate",
|
|
115
|
+
"textLength",
|
|
116
|
+
"lengthAdjust",
|
|
117
|
+
"pathLength",
|
|
118
|
+
"marker-start",
|
|
119
|
+
"marker-mid",
|
|
120
|
+
"marker-end"
|
|
121
|
+
]);
|
|
122
|
+
/** `transform:` CSS property at the start of a declaration list or after `;`. */
|
|
123
|
+
const CSS_TRANSFORM_PROPERTY = /(?:^|;)\s*transform\s*:/i;
|
|
124
|
+
var SvgDocument = class SvgDocument {
|
|
125
|
+
constructor(svg) {
|
|
126
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
127
|
+
this._structure_version = 0;
|
|
128
|
+
this._geometry_version = 0;
|
|
129
|
+
if (typeof svg !== "string") throw new TypeError(`new SvgDocument(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
|
|
130
|
+
this.source = svg;
|
|
131
|
+
const parsed = parse_svg(svg);
|
|
132
|
+
this.original = parsed;
|
|
133
|
+
this.nodes = parsed.nodes;
|
|
134
|
+
this.prolog = parsed.prolog;
|
|
135
|
+
this.epilog = parsed.epilog;
|
|
136
|
+
this.root = parsed.root;
|
|
137
|
+
}
|
|
138
|
+
static parse(svg) {
|
|
139
|
+
return new SvgDocument(svg);
|
|
140
|
+
}
|
|
141
|
+
/** Reload from the original parse, discarding all edits. */
|
|
142
|
+
reset_to_original() {
|
|
143
|
+
const parsed = parse_svg(this.source);
|
|
144
|
+
this.original = parsed;
|
|
145
|
+
this.nodes = parsed.nodes;
|
|
146
|
+
this.prolog = parsed.prolog;
|
|
147
|
+
this.epilog = parsed.epilog;
|
|
148
|
+
this.root = parsed.root;
|
|
149
|
+
this._structure_version++;
|
|
150
|
+
this._geometry_version++;
|
|
151
|
+
this.emit();
|
|
152
|
+
}
|
|
153
|
+
/** Replace document with new svg source (clears edits + history-owned state). */
|
|
154
|
+
load(svg) {
|
|
155
|
+
if (typeof svg !== "string") throw new TypeError(`SvgDocument.load(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
|
|
156
|
+
this.source = svg;
|
|
157
|
+
const parsed = parse_svg(svg);
|
|
158
|
+
this.original = parsed;
|
|
159
|
+
this.nodes = parsed.nodes;
|
|
160
|
+
this.prolog = parsed.prolog;
|
|
161
|
+
this.epilog = parsed.epilog;
|
|
162
|
+
this.root = parsed.root;
|
|
163
|
+
this._structure_version++;
|
|
164
|
+
this._geometry_version++;
|
|
165
|
+
this.emit();
|
|
166
|
+
}
|
|
167
|
+
on_change(fn) {
|
|
168
|
+
this.listeners.add(fn);
|
|
169
|
+
return () => this.listeners.delete(fn);
|
|
170
|
+
}
|
|
171
|
+
/** See `_structure_version` for what this counter signals. */
|
|
172
|
+
get structure_version() {
|
|
173
|
+
return this._structure_version;
|
|
174
|
+
}
|
|
175
|
+
/** See `_geometry_version` for what this counter signals. */
|
|
176
|
+
get geometry_version() {
|
|
177
|
+
return this._geometry_version;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Advance `_geometry_version` by exactly 1 WITHOUT touching the tree,
|
|
181
|
+
* any attribute, `structure_version`, or the `on_change` listeners.
|
|
182
|
+
*
|
|
183
|
+
* The one geometry mutation with no attribute write: a `<text>` /
|
|
184
|
+
* `<tspan>` reflow the IR cannot see — a web font finishing load AFTER
|
|
185
|
+
* the `font-family` / `font-size` write was already serialized. The DOM
|
|
186
|
+
* surface observes the reflow (`document.fonts` `loadingdone`) and asks
|
|
187
|
+
* the geometry channel to advance so the bounds cache re-reads the
|
|
188
|
+
* settled glyph metrics. See ../../docs/geometry.md §Limitations.
|
|
189
|
+
*
|
|
190
|
+
* Deliberately does NOT call `emit()`: this is not a document edit, so
|
|
191
|
+
* it must not bump `doc_version` / mark the doc dirty / touch undo
|
|
192
|
+
* (the editor's `on_change` handler does all three). The editor's
|
|
193
|
+
* `_internal.bump_geometry` advances `geometry_version` here and fans
|
|
194
|
+
* out the geometry listeners itself.
|
|
195
|
+
*/
|
|
196
|
+
bump_geometry() {
|
|
197
|
+
this._geometry_version++;
|
|
198
|
+
}
|
|
199
|
+
emit() {
|
|
200
|
+
for (const fn of this.listeners) fn();
|
|
201
|
+
}
|
|
202
|
+
/** Notify subscribers — for callers that mutate directly via setAttr/etc. */
|
|
203
|
+
notify() {
|
|
204
|
+
this.emit();
|
|
205
|
+
}
|
|
206
|
+
get(id) {
|
|
207
|
+
return this.nodes.get(id) ?? null;
|
|
208
|
+
}
|
|
209
|
+
is_element(id) {
|
|
210
|
+
return this.nodes.get(id)?.kind === "element";
|
|
211
|
+
}
|
|
212
|
+
parent_of(id) {
|
|
213
|
+
return this.nodes.get(id)?.parent ?? null;
|
|
214
|
+
}
|
|
215
|
+
children_of(id) {
|
|
216
|
+
const n = this.nodes.get(id);
|
|
217
|
+
if (!n || n.kind !== "element") return [];
|
|
218
|
+
return n.children;
|
|
219
|
+
}
|
|
220
|
+
/** Element children only — text/comment/cdata filtered out. */
|
|
221
|
+
element_children_of(id) {
|
|
222
|
+
return this.children_of(id).filter((c) => this.is_element(c));
|
|
223
|
+
}
|
|
224
|
+
next_sibling_of(id) {
|
|
225
|
+
const parent = this.parent_of(id);
|
|
68
226
|
if (parent === null) return null;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
227
|
+
const siblings = this.children_of(parent);
|
|
228
|
+
const i = siblings.indexOf(id);
|
|
229
|
+
return i >= 0 && i + 1 < siblings.length ? siblings[i + 1] : null;
|
|
230
|
+
}
|
|
231
|
+
next_element_sibling_of(id) {
|
|
232
|
+
const parent = this.parent_of(id);
|
|
233
|
+
if (parent === null) return null;
|
|
234
|
+
const siblings = this.element_children_of(parent);
|
|
235
|
+
const i = siblings.indexOf(id);
|
|
236
|
+
return i >= 0 && i + 1 < siblings.length ? siblings[i + 1] : null;
|
|
237
|
+
}
|
|
238
|
+
tag_of(id) {
|
|
239
|
+
const n = this.nodes.get(id);
|
|
240
|
+
return n && n.kind === "element" ? n.local : "";
|
|
241
|
+
}
|
|
242
|
+
contains(ancestor, descendant) {
|
|
243
|
+
if (ancestor === descendant) return true;
|
|
244
|
+
let cur = this.parent_of(descendant);
|
|
245
|
+
while (cur !== null) {
|
|
246
|
+
if (cur === ancestor) return true;
|
|
247
|
+
cur = this.parent_of(cur);
|
|
72
248
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Filter a selection down to its **subtree roots** — drop any id whose
|
|
253
|
+
* ancestor is also in the input set.
|
|
254
|
+
*
|
|
255
|
+
* Mirrors `pruneNestedNodes` in the main canvas editor's query module
|
|
256
|
+
* ([editor/grida-canvas/query/index.ts:138](../../../../editor/grida-canvas/query/index.ts)) and shares its UX motivation:
|
|
257
|
+
* when a parent and a descendant are both selected, only the parent
|
|
258
|
+
* should drive multi-node mutations — otherwise the descendant
|
|
259
|
+
* accumulates the transform twice (once via the parent's `transform`,
|
|
260
|
+
* once via its own attribute write). Required for `commands.remove`
|
|
261
|
+
* (avoids re-attaching detached descendants on undo) and any multi-
|
|
262
|
+
* member translate path (avoids 2× drift for the Bar-chart marquee
|
|
263
|
+
* case).
|
|
264
|
+
*
|
|
265
|
+
* Order: preserves the input order for retained ids. Duplicates in
|
|
266
|
+
* the input are not deduplicated — callers are responsible (the
|
|
267
|
+
* editor's `commands.select` already dedupes).
|
|
268
|
+
*
|
|
269
|
+
* Performance: `O(n × depth)`. Builds a `Set` over the input once,
|
|
270
|
+
* then walks each id's ancestor chain at most once. The main editor's
|
|
271
|
+
* version is `O(n² × depth)` (per-pair `isAncestor`) — fine at typical
|
|
272
|
+
* selection sizes (a few dozen), worth winning here for free since
|
|
273
|
+
* `parent_of` is `O(1)` on our parent-map.
|
|
274
|
+
*/
|
|
275
|
+
prune_nested_nodes(ids) {
|
|
276
|
+
if (ids.length <= 1) return [...ids];
|
|
277
|
+
const set = new Set(ids);
|
|
278
|
+
const out = [];
|
|
78
279
|
for (const id of ids) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
280
|
+
let nested = false;
|
|
281
|
+
let cur = this.parent_of(id);
|
|
282
|
+
while (cur !== null) {
|
|
283
|
+
if (set.has(cur)) {
|
|
284
|
+
nested = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
cur = this.parent_of(cur);
|
|
288
|
+
}
|
|
289
|
+
if (!nested) out.push(id);
|
|
82
290
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
all_nodes() {
|
|
294
|
+
const out = [];
|
|
295
|
+
const walk = (id) => {
|
|
296
|
+
out.push(id);
|
|
297
|
+
const c = this.children_of(id);
|
|
298
|
+
for (const ch of c) walk(ch);
|
|
299
|
+
};
|
|
300
|
+
walk(this.root);
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
all_elements() {
|
|
304
|
+
return this.all_nodes().filter((id) => this.is_element(id));
|
|
305
|
+
}
|
|
306
|
+
find_by_tag(ancestor, tag) {
|
|
307
|
+
const out = [];
|
|
308
|
+
const walk = (id) => {
|
|
309
|
+
if (id !== ancestor && this.is_element(id) && this.tag_of(id) === tag) out.push(id);
|
|
310
|
+
for (const c of this.children_of(id)) walk(c);
|
|
311
|
+
};
|
|
312
|
+
walk(ancestor);
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
/** Read attribute by local name, optionally namespace-filtered. */
|
|
316
|
+
get_attr(id, name, ns = null) {
|
|
317
|
+
const n = this.nodes.get(id);
|
|
318
|
+
if (!n || n.kind !== "element") return null;
|
|
319
|
+
for (const a of n.attrs) if (a.local === name && (ns === null || a.ns === ns)) return a.value;
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Set / remove an attribute. If the attribute exists, it is mutated in place
|
|
324
|
+
* (preserving source position). If it doesn't, it's appended.
|
|
325
|
+
*/
|
|
326
|
+
set_attr(id, name, value, ns = null) {
|
|
327
|
+
const n = this.nodes.get(id);
|
|
328
|
+
if (!n || n.kind !== "element") return;
|
|
329
|
+
const structural = name === "id";
|
|
330
|
+
const geometry = ns === null && GEOMETRY_ATTRS.has(name);
|
|
331
|
+
for (let i = 0; i < n.attrs.length; i++) {
|
|
332
|
+
const a = n.attrs[i];
|
|
333
|
+
if (a.local === name && (ns === null || a.ns === ns)) {
|
|
334
|
+
if (value === null) n.attrs.splice(i, 1);
|
|
335
|
+
else a.value = value;
|
|
336
|
+
if (structural) this._structure_version++;
|
|
337
|
+
if (geometry) this._geometry_version++;
|
|
338
|
+
this.emit();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (value !== null) {
|
|
343
|
+
const prefix = ns === XLINK_NS$1 ? "xlink" : ns === XMLNS_NS ? "xmlns" : null;
|
|
344
|
+
n.attrs.push({
|
|
345
|
+
raw_name: prefix ? `${prefix}:${name}` : name,
|
|
346
|
+
prefix,
|
|
347
|
+
local: name,
|
|
348
|
+
ns,
|
|
349
|
+
value,
|
|
350
|
+
pre: " ",
|
|
351
|
+
eq_trivia: "",
|
|
352
|
+
quote: "\""
|
|
353
|
+
});
|
|
354
|
+
if (structural) this._structure_version++;
|
|
355
|
+
if (geometry) this._geometry_version++;
|
|
356
|
+
this.emit();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
attributes_of(id) {
|
|
360
|
+
const n = this.nodes.get(id);
|
|
361
|
+
if (!n || n.kind !== "element") return [];
|
|
362
|
+
return n.attrs.map((a) => ({
|
|
363
|
+
name: a.local,
|
|
364
|
+
ns: a.ns,
|
|
365
|
+
value: a.value
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
get_style(id, property) {
|
|
369
|
+
const style = this.get_attr(id, "style");
|
|
370
|
+
if (!style) return null;
|
|
371
|
+
const decls = parse_inline_style(style);
|
|
372
|
+
for (const d of decls) if (d.property === property) return d.value;
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
set_style(id, property, value) {
|
|
376
|
+
const decls = parse_inline_style(this.get_attr(id, "style") ?? "");
|
|
377
|
+
const idx = decls.findIndex((d) => d.property === property);
|
|
378
|
+
if (value === null) {
|
|
379
|
+
if (idx === -1) return;
|
|
380
|
+
decls.splice(idx, 1);
|
|
381
|
+
} else if (idx === -1) decls.push({
|
|
382
|
+
property,
|
|
383
|
+
value
|
|
384
|
+
});
|
|
385
|
+
else decls[idx].value = value;
|
|
386
|
+
const next = decls.map((d) => `${d.property}: ${d.value}`).join("; ");
|
|
387
|
+
this.set_attr(id, "style", next === "" ? null : next);
|
|
388
|
+
}
|
|
389
|
+
get_all_styles(id) {
|
|
390
|
+
const style = this.get_attr(id, "style");
|
|
391
|
+
if (!style) return [];
|
|
392
|
+
return parse_inline_style(style);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Whether `id` can be opened in the flat-string text editor.
|
|
396
|
+
*
|
|
397
|
+
* v1 contract: the editor only operates on a *single flat text run*. That
|
|
398
|
+
* means the target must be a `<text>` or `<tspan>` whose direct children
|
|
399
|
+
* are all text nodes (or it has no children). A `<text>` containing a
|
|
400
|
+
* `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
|
|
401
|
+
* content from the editor's view, and a flat-text write would leave the
|
|
402
|
+
* tspan dangling. Tspan-as-target is fine and well-defined when it's a
|
|
403
|
+
* leaf; only the host decides whether to route double-click to a tspan
|
|
404
|
+
* or its parent text.
|
|
405
|
+
*/
|
|
406
|
+
is_text_edit_target(id) {
|
|
407
|
+
const n = this.nodes.get(id);
|
|
408
|
+
if (!n || n.kind !== "element") return false;
|
|
409
|
+
if (n.local !== "text" && n.local !== "tspan") return false;
|
|
410
|
+
for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Returns a tag-discriminated snapshot of the authored geometry attrs
|
|
415
|
+
* if this node is eligible for vector (vertex) editing — else `null`.
|
|
416
|
+
*
|
|
417
|
+
* Eligibility:
|
|
418
|
+
* - `<path>` — requires non-empty `d`.
|
|
419
|
+
* - `<line>` — requires two distinct finite user-unit endpoints.
|
|
420
|
+
* - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
|
|
421
|
+
* - `<polygon>` — same as polyline.
|
|
422
|
+
* - `<rect>` — requires finite user-unit `width`/`height` > 0.
|
|
423
|
+
* - `<circle>` — requires finite user-unit `r` > 0.
|
|
424
|
+
* - `<ellipse>` — requires finite user-unit `rx`/`ry` > 0.
|
|
425
|
+
*
|
|
426
|
+
* The vertex tags (`line` / `polyline` / `polygon`) write edits back to
|
|
427
|
+
* their native attributes while the geometry stays expressible there; an
|
|
428
|
+
* edit that escapes the native form (a curve, or a topology change that
|
|
429
|
+
* leaves the canonical chain) re-types the element to `<path>`. The
|
|
430
|
+
* geometry primitives (`rect` / `circle` / `ellipse`) have no native
|
|
431
|
+
* vector form, so any vector edit re-types them. In all cases the native
|
|
432
|
+
* tag is preserved byte-for-byte until the first re-typing edit commits
|
|
433
|
+
* (see `retype_to_path`). Design:
|
|
434
|
+
* `docs/wg/feat-svg-editor/promote-to-path.md`.
|
|
435
|
+
*
|
|
436
|
+
* Geometry that is not a plain user-unit number (`%`, `px`, `em`, …) is
|
|
437
|
+
* an out-of-scope gap, so such an element returns `null` rather than
|
|
438
|
+
* advertising an edit the editor cannot perform faithfully.
|
|
439
|
+
*
|
|
440
|
+
* Rejects `<image>` / `<use>` (raster / reference bounding boxes, no
|
|
441
|
+
* editable outline).
|
|
442
|
+
*/
|
|
443
|
+
/**
|
|
444
|
+
* Parse an optional SVG geometry coordinate (`x`/`y`, `cx`/`cy`, the line
|
|
445
|
+
* endpoints). An **absent** attribute takes the SVG default (`0`); a
|
|
446
|
+
* **present** attribute that is not a plain user-unit number (`%`, `px`,
|
|
447
|
+
* `em`, …) is out of scope and yields `null` so the caller refuses the
|
|
448
|
+
* element — the same gate required attrs (width / radius) already apply.
|
|
449
|
+
*
|
|
450
|
+
* The absent-vs-present distinction is the point: a bare `?? 0` would
|
|
451
|
+
* silently coerce an authored `x1="5px"` to `0`, then the first native
|
|
452
|
+
* writeback would overwrite that authored value. Refusing keeps the
|
|
453
|
+
* editor from misrepresenting geometry it cannot read faithfully.
|
|
454
|
+
*/
|
|
455
|
+
optional_user_unit_coord(id, name) {
|
|
456
|
+
const raw = this.get_attr(id, name);
|
|
457
|
+
if (raw === null) return 0;
|
|
458
|
+
return parse_user_unit(raw);
|
|
459
|
+
}
|
|
460
|
+
is_vector_edit_target(id) {
|
|
461
|
+
const n = this.nodes.get(id);
|
|
462
|
+
if (!n || n.kind !== "element") return null;
|
|
463
|
+
if (RETYPABLE_GEOMETRY_ATTRS[n.local] && n.attrs.some((a) => a.prefix === null && a.ns === null && a.local === "d")) return null;
|
|
464
|
+
switch (n.local) {
|
|
465
|
+
case "path": {
|
|
466
|
+
const d = this.get_attr(id, "d");
|
|
467
|
+
if (d === null || d.trim().length === 0) return null;
|
|
468
|
+
return {
|
|
469
|
+
kind: "path",
|
|
470
|
+
d
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
case "line": {
|
|
474
|
+
const x1 = this.optional_user_unit_coord(id, "x1");
|
|
475
|
+
const y1 = this.optional_user_unit_coord(id, "y1");
|
|
476
|
+
const x2 = this.optional_user_unit_coord(id, "x2");
|
|
477
|
+
const y2 = this.optional_user_unit_coord(id, "y2");
|
|
478
|
+
if (x1 === null || y1 === null || x2 === null || y2 === null) return null;
|
|
479
|
+
if (x1 === x2 && y1 === y2) return null;
|
|
480
|
+
return {
|
|
481
|
+
kind: "line",
|
|
482
|
+
x1,
|
|
483
|
+
y1,
|
|
484
|
+
x2,
|
|
485
|
+
y2
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
case "polyline":
|
|
489
|
+
case "polygon": {
|
|
490
|
+
const raw = this.get_attr(id, "points") ?? "";
|
|
491
|
+
const parsed = svg_parse.parse_points(raw);
|
|
492
|
+
if (parsed.length < 2) return null;
|
|
493
|
+
const points = parsed.map((p) => [p.x, p.y]);
|
|
494
|
+
return n.local === "polyline" ? {
|
|
495
|
+
kind: "polyline",
|
|
496
|
+
points
|
|
497
|
+
} : {
|
|
498
|
+
kind: "polygon",
|
|
499
|
+
points
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
case "rect": {
|
|
503
|
+
const x = this.optional_user_unit_coord(id, "x");
|
|
504
|
+
const y = this.optional_user_unit_coord(id, "y");
|
|
505
|
+
if (x === null || y === null) return null;
|
|
506
|
+
const width = parse_user_unit(this.get_attr(id, "width"));
|
|
507
|
+
const height = parse_user_unit(this.get_attr(id, "height"));
|
|
508
|
+
if (width === null || height === null) return null;
|
|
509
|
+
if (width <= 0 || height <= 0) return null;
|
|
510
|
+
const rx_attr = this.get_attr(id, "rx");
|
|
511
|
+
const ry_attr = this.get_attr(id, "ry");
|
|
512
|
+
const rx_parsed = rx_attr === null ? null : parse_user_unit(rx_attr);
|
|
513
|
+
const ry_parsed = ry_attr === null ? null : parse_user_unit(ry_attr);
|
|
514
|
+
if (rx_attr !== null && rx_parsed === null) return null;
|
|
515
|
+
if (ry_attr !== null && ry_parsed === null) return null;
|
|
516
|
+
let rx = rx_parsed ?? ry_parsed ?? 0;
|
|
517
|
+
let ry = ry_parsed ?? rx_parsed ?? 0;
|
|
518
|
+
rx = Math.max(0, Math.min(rx, width / 2));
|
|
519
|
+
ry = Math.max(0, Math.min(ry, height / 2));
|
|
520
|
+
return {
|
|
521
|
+
kind: "rect",
|
|
522
|
+
x,
|
|
523
|
+
y,
|
|
524
|
+
width,
|
|
525
|
+
height,
|
|
526
|
+
rx,
|
|
527
|
+
ry
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
case "circle": {
|
|
531
|
+
const cx = this.optional_user_unit_coord(id, "cx");
|
|
532
|
+
const cy = this.optional_user_unit_coord(id, "cy");
|
|
533
|
+
if (cx === null || cy === null) return null;
|
|
534
|
+
const r = parse_user_unit(this.get_attr(id, "r"));
|
|
535
|
+
if (r === null || r <= 0) return null;
|
|
536
|
+
return {
|
|
537
|
+
kind: "circle",
|
|
538
|
+
cx,
|
|
539
|
+
cy,
|
|
540
|
+
r
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
case "ellipse": {
|
|
544
|
+
const cx = this.optional_user_unit_coord(id, "cx");
|
|
545
|
+
const cy = this.optional_user_unit_coord(id, "cy");
|
|
546
|
+
if (cx === null || cy === null) return null;
|
|
547
|
+
const rx = parse_user_unit(this.get_attr(id, "rx"));
|
|
548
|
+
const ry = parse_user_unit(this.get_attr(id, "ry"));
|
|
549
|
+
if (rx === null || ry === null) return null;
|
|
550
|
+
if (rx <= 0 || ry <= 0) return null;
|
|
551
|
+
return {
|
|
552
|
+
kind: "ellipse",
|
|
553
|
+
cx,
|
|
554
|
+
cy,
|
|
555
|
+
rx,
|
|
556
|
+
ry
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
default: return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Re-type a native vector element (`<line>` / `<polyline>` / `<polygon>` /
|
|
564
|
+
* `<rect>` / `<circle>` / `<ellipse>`) into a `<path>` in place, consuming
|
|
565
|
+
* its native geometry attributes and setting `d`. A structural mutation:
|
|
566
|
+
* this layer executes the re-type; it does not decide when one is
|
|
567
|
+
* warranted.
|
|
568
|
+
*
|
|
569
|
+
* Idempotent: returns `null` if `id` is not currently one of those tags
|
|
570
|
+
* (so it is safe to call repeatedly — once re-typed, e.g. already a
|
|
571
|
+
* `<path>`, further calls are no-ops). Otherwise mutates the node and
|
|
572
|
+
* returns an opaque {@link RetypeRecord} reversal token.
|
|
573
|
+
*
|
|
574
|
+
* Identity, children, `self_closing`, non-geometry attributes, and all
|
|
575
|
+
* source trivia are preserved unchanged — only the tag and the geometry
|
|
576
|
+
* attributes move. Pass the token to {@link revert_retype} to restore
|
|
577
|
+
* the original primitive byte-for-byte.
|
|
578
|
+
*
|
|
579
|
+
* (see test/svg-editor-vector-promote-to-path.md)
|
|
580
|
+
*/
|
|
581
|
+
retype_to_path(id, d) {
|
|
582
|
+
const n = this.nodes.get(id);
|
|
583
|
+
if (!n || n.kind !== "element") return null;
|
|
584
|
+
const geom = RETYPABLE_GEOMETRY_ATTRS[n.local];
|
|
585
|
+
if (!geom) return null;
|
|
586
|
+
const prev_local = n.local;
|
|
587
|
+
const prev_raw_tag = n.raw_tag;
|
|
588
|
+
const removed = [];
|
|
589
|
+
for (let i = n.attrs.length - 1; i >= 0; i--) {
|
|
590
|
+
const a = n.attrs[i];
|
|
591
|
+
if (a.prefix === null && a.ns === null && geom.has(a.local)) {
|
|
592
|
+
removed.push({
|
|
593
|
+
index: i,
|
|
594
|
+
token: a
|
|
595
|
+
});
|
|
596
|
+
n.attrs.splice(i, 1);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
removed.reverse();
|
|
600
|
+
n.local = "path";
|
|
601
|
+
n.raw_tag = n.prefix ? `${n.prefix}:path` : "path";
|
|
602
|
+
n.attrs.push({
|
|
603
|
+
raw_name: "d",
|
|
604
|
+
prefix: null,
|
|
605
|
+
local: "d",
|
|
606
|
+
ns: null,
|
|
607
|
+
value: d,
|
|
608
|
+
pre: " ",
|
|
609
|
+
eq_trivia: "",
|
|
610
|
+
quote: "\""
|
|
611
|
+
});
|
|
612
|
+
let added_fill_none = false;
|
|
613
|
+
if (prev_local === "line" && this.get_attr(id, "fill") === null && this.get_style(id, "fill") === null) {
|
|
614
|
+
n.attrs.push({
|
|
615
|
+
raw_name: "fill",
|
|
616
|
+
prefix: null,
|
|
617
|
+
local: "fill",
|
|
618
|
+
ns: null,
|
|
619
|
+
value: "none",
|
|
620
|
+
pre: " ",
|
|
621
|
+
eq_trivia: "",
|
|
622
|
+
quote: "\""
|
|
623
|
+
});
|
|
624
|
+
added_fill_none = true;
|
|
625
|
+
}
|
|
626
|
+
this._structure_version++;
|
|
627
|
+
this._geometry_version++;
|
|
628
|
+
this.emit();
|
|
89
629
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
630
|
+
prev_local,
|
|
631
|
+
prev_raw_tag,
|
|
632
|
+
removed,
|
|
633
|
+
added_fill_none
|
|
94
634
|
};
|
|
95
635
|
}
|
|
96
|
-
|
|
97
|
-
}
|
|
636
|
+
/**
|
|
637
|
+
* Reverse a {@link retype_to_path}: restore the original tag, remove the
|
|
638
|
+
* `d` attribute the promotion added, and splice the captured geometry
|
|
639
|
+
* attribute tokens back at their original positions (preserving their
|
|
640
|
+
* trivia, so a later `serialize()` is byte-equal to the pre-promotion
|
|
641
|
+
* source).
|
|
642
|
+
*/
|
|
643
|
+
revert_retype(id, token) {
|
|
644
|
+
const n = this.nodes.get(id);
|
|
645
|
+
if (!n || n.kind !== "element") return;
|
|
646
|
+
for (let i = n.attrs.length - 1; i >= 0; i--) {
|
|
647
|
+
const a = n.attrs[i];
|
|
648
|
+
if (a.prefix === null && a.ns === null && a.local === "d") {
|
|
649
|
+
n.attrs.splice(i, 1);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (token.added_fill_none) for (let i = n.attrs.length - 1; i >= 0; i--) {
|
|
654
|
+
const a = n.attrs[i];
|
|
655
|
+
if (a.prefix === null && a.ns === null && a.local === "fill") {
|
|
656
|
+
n.attrs.splice(i, 1);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
n.local = token.prev_local;
|
|
661
|
+
n.raw_tag = token.prev_raw_tag;
|
|
662
|
+
for (const { index, token: t } of token.removed) n.attrs.splice(index, 0, t);
|
|
663
|
+
this._structure_version++;
|
|
664
|
+
this._geometry_version++;
|
|
665
|
+
this.emit();
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
|
|
669
|
+
* per-glyph attribute (which conflicts with element-level rotation).
|
|
670
|
+
*/
|
|
671
|
+
has_glyph_rotate(id) {
|
|
672
|
+
const tag = this.tag_of(id);
|
|
673
|
+
if (tag !== "text" && tag !== "tspan") return false;
|
|
674
|
+
const value = this.get_attr(id, "rotate");
|
|
675
|
+
if (value === null) return false;
|
|
676
|
+
return value.trim() !== "";
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* True iff this element's inline `style=""` declares a `transform:`
|
|
680
|
+
* CSS property (which would shadow the editor's `transform=` writes).
|
|
681
|
+
*/
|
|
682
|
+
has_inline_css_transform(id) {
|
|
683
|
+
const style = this.get_attr(id, "style");
|
|
684
|
+
if (!style) return false;
|
|
685
|
+
return CSS_TRANSFORM_PROPERTY.test(style);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* True iff this element has a direct `<animateTransform>` child
|
|
689
|
+
* (which produces a time-varying transform invisible to attribute writes).
|
|
690
|
+
* Only direct children are checked — nested cases attach to the nearer ancestor.
|
|
691
|
+
*/
|
|
692
|
+
has_animate_transform_child(id) {
|
|
693
|
+
for (const c of this.children_of(id)) {
|
|
694
|
+
const n = this.nodes.get(c);
|
|
695
|
+
if (n?.kind === "element" && n.local === "animateTransform") return true;
|
|
696
|
+
}
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
text_of(id) {
|
|
700
|
+
const n = this.nodes.get(id);
|
|
701
|
+
if (!n || n.kind !== "element") return "";
|
|
702
|
+
let out = "";
|
|
703
|
+
for (const c of n.children) {
|
|
704
|
+
const cn = this.nodes.get(c);
|
|
705
|
+
if (cn?.kind === "text") out += cn.value;
|
|
706
|
+
}
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
/** Replace all direct text children with a single text node carrying `value`. */
|
|
710
|
+
set_text(id, value) {
|
|
711
|
+
const n = this.nodes.get(id);
|
|
712
|
+
if (!n || n.kind !== "element") return;
|
|
713
|
+
n.children = n.children.filter((c) => this.nodes.get(c)?.kind !== "text");
|
|
714
|
+
if (value !== "") {
|
|
715
|
+
const text_id = `t${Math.random().toString(36).slice(2, 10)}`;
|
|
716
|
+
const text_node = {
|
|
717
|
+
kind: "text",
|
|
718
|
+
id: text_id,
|
|
719
|
+
parent: id,
|
|
720
|
+
value
|
|
721
|
+
};
|
|
722
|
+
this.nodes.set(text_id, text_node);
|
|
723
|
+
n.children.push(text_id);
|
|
724
|
+
}
|
|
725
|
+
this._structure_version++;
|
|
726
|
+
this._geometry_version++;
|
|
727
|
+
this.emit();
|
|
728
|
+
}
|
|
729
|
+
insert(id, parent, before) {
|
|
730
|
+
const node = this.nodes.get(id);
|
|
731
|
+
const parent_node = this.nodes.get(parent);
|
|
732
|
+
if (!node || !parent_node || parent_node.kind !== "element") return;
|
|
733
|
+
if (node.parent !== null) {
|
|
734
|
+
const old_parent = this.nodes.get(node.parent);
|
|
735
|
+
if (old_parent && old_parent.kind === "element") {
|
|
736
|
+
const i = old_parent.children.indexOf(id);
|
|
737
|
+
if (i >= 0) old_parent.children.splice(i, 1);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const ix = before === null ? -1 : parent_node.children.indexOf(before);
|
|
741
|
+
if (ix < 0) parent_node.children.push(id);
|
|
742
|
+
else parent_node.children.splice(ix, 0, id);
|
|
743
|
+
node.parent = parent;
|
|
744
|
+
this._structure_version++;
|
|
745
|
+
this._geometry_version++;
|
|
746
|
+
this.emit();
|
|
747
|
+
}
|
|
748
|
+
remove(id) {
|
|
749
|
+
const n = this.nodes.get(id);
|
|
750
|
+
if (!n || n.parent === null) return;
|
|
751
|
+
const parent = this.nodes.get(n.parent);
|
|
752
|
+
if (!parent || parent.kind !== "element") return;
|
|
753
|
+
const i = parent.children.indexOf(id);
|
|
754
|
+
if (i >= 0) parent.children.splice(i, 1);
|
|
755
|
+
n.parent = null;
|
|
756
|
+
this._structure_version++;
|
|
757
|
+
this._geometry_version++;
|
|
758
|
+
this.emit();
|
|
759
|
+
}
|
|
760
|
+
/** Create a new element node and register it (not yet inserted). */
|
|
761
|
+
create_element(local, opts) {
|
|
762
|
+
const id = this.fresh_node_id();
|
|
763
|
+
const prefix = opts?.prefix ?? null;
|
|
764
|
+
const ns = opts?.ns ?? null;
|
|
765
|
+
const node = {
|
|
766
|
+
kind: "element",
|
|
767
|
+
id,
|
|
768
|
+
parent: null,
|
|
769
|
+
raw_tag: prefix ? `${prefix}:${local}` : local,
|
|
770
|
+
prefix,
|
|
771
|
+
local,
|
|
772
|
+
ns,
|
|
773
|
+
attrs: [],
|
|
774
|
+
children: [],
|
|
775
|
+
self_closing: false,
|
|
776
|
+
open_tag_trailing: "",
|
|
777
|
+
close_tag_leading: "",
|
|
778
|
+
close_tag_trailing: ""
|
|
779
|
+
};
|
|
780
|
+
this.nodes.set(id, node);
|
|
781
|
+
return id;
|
|
782
|
+
}
|
|
783
|
+
/** Fresh internal NodeId, guaranteed unique within this document's node
|
|
784
|
+
* map. Shared by `create_element` and fragment adoption — collisions
|
|
785
|
+
* matter for the latter because the parser assigns sequential per-parse
|
|
786
|
+
* ids that a second parse would repeat. */
|
|
787
|
+
fresh_node_id() {
|
|
788
|
+
let id;
|
|
789
|
+
do
|
|
790
|
+
id = `e${Math.random().toString(36).slice(2, 10)}`;
|
|
791
|
+
while (this.nodes.has(id));
|
|
792
|
+
return id;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Parse an SVG **fragment** string and adopt its element subtrees into
|
|
796
|
+
* this document's node store — registered like {@link create_element}
|
|
797
|
+
* but NOT inserted into the tree (no version bump, no emit). Callers
|
|
798
|
+
* attach the returned roots via {@link insert}; the editor's
|
|
799
|
+
* `commands.insert_fragment` is the history-bracketed consumer.
|
|
800
|
+
*
|
|
801
|
+
* Input shapes:
|
|
802
|
+
* - A **bare fragment** — one or more sibling elements
|
|
803
|
+
* (`<path …/><path …/>`, or a single `<g>…</g>`). The top-level
|
|
804
|
+
* elements become the returned roots, in source order.
|
|
805
|
+
* - A **full SVG document** — when the input's only top-level element
|
|
806
|
+
* is an `<svg>`, that element is treated as a document SHELL, not
|
|
807
|
+
* content: its element children become the roots and the shell
|
|
808
|
+
* itself (viewBox, width/height, prolog, doctype) is discarded. Its
|
|
809
|
+
* `xmlns:*` prefix declarations are harvested into `xmlns` so the
|
|
810
|
+
* caller can re-declare prefixes the adopted content still uses.
|
|
811
|
+
* An `<svg>` that appears as one of SEVERAL top-level elements (or
|
|
812
|
+
* anywhere below the top level) is content, adopted as-is.
|
|
813
|
+
*
|
|
814
|
+
* Top-level non-element nodes (whitespace between roots, comments, PIs,
|
|
815
|
+
* doctype) are dropped — adoption takes elements, and the host
|
|
816
|
+
* document's own trivia stays untouched. WITHIN each adopted subtree
|
|
817
|
+
* every byte of source trivia survives verbatim (attribute order, quote
|
|
818
|
+
* styles, whitespace, comments), so the inserted markup serializes back
|
|
819
|
+
* exactly as authored — same rules as the initial parse.
|
|
820
|
+
*
|
|
821
|
+
* Authored `id=""` attributes are adopted verbatim — never rewritten,
|
|
822
|
+
* even when they collide with ids already in the document. Silent id
|
|
823
|
+
* renaming is exactly the proprietary noise this editor refuses (README
|
|
824
|
+
* "What clean means" §3); deduplication belongs to the explicit Tidy
|
|
825
|
+
* command. Internal NodeIds ARE freshly assigned (see
|
|
826
|
+
* {@link fresh_node_id}) so adopted nodes never collide in the id map.
|
|
827
|
+
*
|
|
828
|
+
* Throws `TypeError` on a non-string input and `Error` on markup the
|
|
829
|
+
* parser rejects (unclosed / mismatched tags, malformed attributes). An
|
|
830
|
+
* input with no top-level elements (empty string, whitespace, comments
|
|
831
|
+
* only) returns `{ roots: [], xmlns: [] }`.
|
|
832
|
+
*/
|
|
833
|
+
create_fragment(markup) {
|
|
834
|
+
if (typeof markup !== "string") throw new TypeError(`create_fragment(markup) requires a string source, got ${markup === null ? "null" : typeof markup}`);
|
|
835
|
+
const parsed = parse_svg(`<svg xmlns="${SVG_NS}" xmlns:xlink="${XLINK_NS$1}">${markup}</svg>`);
|
|
836
|
+
const wrapper = parsed.nodes.get(parsed.root);
|
|
837
|
+
const element_children = (n) => n.children.map((c) => parsed.nodes.get(c)).filter((cn) => cn?.kind === "element");
|
|
838
|
+
let content = element_children(wrapper);
|
|
839
|
+
const xmlns = [];
|
|
840
|
+
if (content.length === 1 && content[0].local === "svg") {
|
|
841
|
+
const shell = content[0];
|
|
842
|
+
for (const a of shell.attrs) if (a.prefix === "xmlns") xmlns.push({
|
|
843
|
+
prefix: a.local,
|
|
844
|
+
uri: a.value
|
|
845
|
+
});
|
|
846
|
+
content = element_children(shell);
|
|
847
|
+
}
|
|
848
|
+
const roots = [];
|
|
849
|
+
for (const node of content) roots.push(this.adopt_parsed_subtree(node, parsed.nodes, null));
|
|
850
|
+
return {
|
|
851
|
+
roots,
|
|
852
|
+
xmlns
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Register `node` and its whole subtree (from a foreign parse) into this
|
|
857
|
+
* document's node map under fresh NodeIds. The parser assigns sequential
|
|
858
|
+
* per-parse ids (`n0`, `n1`, …), so adopting without a remap would
|
|
859
|
+
* collide with this document's own nodes. Children links are rewritten;
|
|
860
|
+
* the subtree root arrives detached (`parent: null`), like
|
|
861
|
+
* `create_element`. Mutates the parsed nodes in place — a parse result
|
|
862
|
+
* is single-use.
|
|
863
|
+
*/
|
|
864
|
+
adopt_parsed_subtree(node, source, parent) {
|
|
865
|
+
const id = this.fresh_node_id();
|
|
866
|
+
node.id = id;
|
|
867
|
+
node.parent = parent;
|
|
868
|
+
this.nodes.set(id, node);
|
|
869
|
+
if (node.kind === "element") {
|
|
870
|
+
const parsed_children = node.children;
|
|
871
|
+
node.children = [];
|
|
872
|
+
for (const c of parsed_children) {
|
|
873
|
+
const child = source.get(c);
|
|
874
|
+
if (!child) continue;
|
|
875
|
+
node.children.push(this.adopt_parsed_subtree(child, source, id));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return id;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Namespace prefixes USED within `id`'s subtree (element tags and
|
|
882
|
+
* attribute names) that are not DECLARED within the subtree itself —
|
|
883
|
+
* i.e. prefixes the subtree borrows from ancestor scope. `xml` and
|
|
884
|
+
* `xmlns` are excluded (bound by the XML spec, never declared).
|
|
885
|
+
* Declaration scoping is honored per use-site: a prefix declared on the
|
|
886
|
+
* using element or any of its ancestors up to (and including) the
|
|
887
|
+
* subtree root counts as declared.
|
|
888
|
+
*
|
|
889
|
+
* Structural fact only — the caller decides what an unbound prefix
|
|
890
|
+
* means (e.g. `commands.insert_fragment` hoists a resolvable
|
|
891
|
+
* declaration onto the document root).
|
|
892
|
+
*/
|
|
893
|
+
undeclared_ns_prefixes(id) {
|
|
894
|
+
const out = /* @__PURE__ */ new Set();
|
|
895
|
+
const walk = (nid, declared) => {
|
|
896
|
+
const n = this.nodes.get(nid);
|
|
897
|
+
if (!n || n.kind !== "element") return;
|
|
898
|
+
const scope = new Set(declared);
|
|
899
|
+
for (const a of n.attrs) if (a.prefix === "xmlns") scope.add(a.local);
|
|
900
|
+
const need = (p) => {
|
|
901
|
+
if (p === null || p === "xml" || p === "xmlns") return;
|
|
902
|
+
if (!scope.has(p)) out.add(p);
|
|
903
|
+
};
|
|
904
|
+
need(n.prefix);
|
|
905
|
+
for (const a of n.attrs) if (a.prefix !== "xmlns") need(a.prefix);
|
|
906
|
+
for (const c of n.children) walk(c, scope);
|
|
907
|
+
};
|
|
908
|
+
walk(id, /* @__PURE__ */ new Set());
|
|
909
|
+
return out;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Declare a namespace prefix on the ROOT element: appends
|
|
913
|
+
* `xmlns:<prefix>="<uri>"` when the root doesn't already declare that
|
|
914
|
+
* prefix. An authored declaration always wins — this never rebinds.
|
|
915
|
+
* Policy wrapper over {@link set_attr} in the `XMLNS_NS` space; removal
|
|
916
|
+
* works through `set_attr(root, prefix, null, XMLNS_NS)` as usual.
|
|
917
|
+
*/
|
|
918
|
+
declare_xmlns(prefix, uri) {
|
|
919
|
+
if (this.get_attr(this.root, prefix, XMLNS_NS) !== null) return;
|
|
920
|
+
this.set_attr(this.root, prefix, uri, XMLNS_NS);
|
|
921
|
+
}
|
|
922
|
+
serialize() {
|
|
923
|
+
let out = "";
|
|
924
|
+
for (const p of this.prolog) out += this.emit_node(p);
|
|
925
|
+
out += this.emit_node(this.nodes.get(this.root));
|
|
926
|
+
for (const e of this.epilog) out += this.emit_node(e);
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Serialize a single element's subtree as an SVG **fragment**, using the
|
|
931
|
+
* same trivia-preserving rules as {@link serialize} (attribute order,
|
|
932
|
+
* quote style, whitespace, comments — emitted exactly as authored).
|
|
933
|
+
*
|
|
934
|
+
* This is NOT {@link serialize} scoped to a node — it is a deliberately
|
|
935
|
+
* weaker output (sdk-design D3, asymmetric outputs stay separate):
|
|
936
|
+
*
|
|
937
|
+
* - `serialize()` emits the whole document and carries the P1
|
|
938
|
+
* whole-document round-trip guarantee.
|
|
939
|
+
* - `serialize_node()` emits a fragment and does NOT. Namespace
|
|
940
|
+
* declarations that live on an ancestor (`xmlns:xlink` and friends,
|
|
941
|
+
* normally on the root `<svg>`) are NOT inlined — a node using
|
|
942
|
+
* `xlink:href` serializes without `xmlns:xlink`. The fragment is the
|
|
943
|
+
* element's markup as authored, not a standalone parseable document.
|
|
944
|
+
*
|
|
945
|
+
* Throws on an unknown id, a non-element node, or a node detached from
|
|
946
|
+
* the live tree: the contract is "the markup for a selected element,"
|
|
947
|
+
* selections are always live elements, and a string return of `""` for a
|
|
948
|
+
* bad id would hide consumer bugs. The detached case matters because
|
|
949
|
+
* `remove()` keeps the node in the id map for undo — a stale id from a
|
|
950
|
+
* removed node would otherwise serialize content no longer in the
|
|
951
|
+
* document, silently feeding a consumer deleted markup.
|
|
952
|
+
*/
|
|
953
|
+
serialize_node(id) {
|
|
954
|
+
const n = this.nodes.get(id);
|
|
955
|
+
if (!n) throw new Error(`serialize_node: unknown node id ${JSON.stringify(id)}`);
|
|
956
|
+
if (n.kind !== "element") throw new Error(`serialize_node: node ${JSON.stringify(id)} is a ${n.kind} node, not an element`);
|
|
957
|
+
if (!this.contains(this.root, id)) throw new Error(`serialize_node: node ${JSON.stringify(id)} is detached from the current document`);
|
|
958
|
+
return this.emit_node(n);
|
|
959
|
+
}
|
|
960
|
+
emit_node(n) {
|
|
961
|
+
switch (n.kind) {
|
|
962
|
+
case "text": return encode_text(n.value);
|
|
963
|
+
case "comment": return `<!--${n.value}-->`;
|
|
964
|
+
case "cdata": return `<![CDATA[${n.value}]]>`;
|
|
965
|
+
case "pi": {
|
|
966
|
+
const pi = n;
|
|
967
|
+
return `<?${pi.target}${pi.value ? " " + pi.value : ""}?>`;
|
|
968
|
+
}
|
|
969
|
+
case "doctype": return `<!DOCTYPE${n.value}>`;
|
|
970
|
+
case "element": {
|
|
971
|
+
const e = n;
|
|
972
|
+
let s = `<${e.raw_tag}`;
|
|
973
|
+
for (const a of e.attrs) s += this.emit_attr(a);
|
|
974
|
+
if (e.children.length === 0 && e.self_closing) {
|
|
975
|
+
s += `${e.open_tag_trailing}/>`;
|
|
976
|
+
return s;
|
|
977
|
+
}
|
|
978
|
+
s += `${e.open_tag_trailing}>`;
|
|
979
|
+
for (const cid of e.children) {
|
|
980
|
+
const cn = this.nodes.get(cid);
|
|
981
|
+
if (cn) s += this.emit_node(cn);
|
|
982
|
+
}
|
|
983
|
+
s += `</${e.close_tag_leading}${e.raw_tag}${e.close_tag_trailing}>`;
|
|
984
|
+
return s;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
emit_attr(a) {
|
|
989
|
+
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.quote}${encode_attr_value(a.value, a.quote)}${a.quote}`;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
function parse_inline_style(s) {
|
|
993
|
+
const out = [];
|
|
994
|
+
const decls = s.split(";");
|
|
995
|
+
for (const decl of decls) {
|
|
996
|
+
const colon = decl.indexOf(":");
|
|
997
|
+
if (colon === -1) continue;
|
|
998
|
+
const property = decl.slice(0, colon).trim();
|
|
999
|
+
const value = decl.slice(colon + 1).trim();
|
|
1000
|
+
if (property) out.push({
|
|
1001
|
+
property,
|
|
1002
|
+
value
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
98
1007
|
//#endregion
|
|
99
1008
|
//#region src/core/transform.ts
|
|
100
1009
|
let transform;
|
|
@@ -332,8 +1241,186 @@ let transform;
|
|
|
332
1241
|
} : op));
|
|
333
1242
|
}
|
|
334
1243
|
_transform.recompose = recompose;
|
|
1244
|
+
/** Epsilon for the identity-leading-matrix drop. Trig/compose noise on
|
|
1245
|
+
* a flip-then-flip round-trip lands well inside 1e-9; authored SVG
|
|
1246
|
+
* coordinates rarely carry more than 4 significant decimals, so this
|
|
1247
|
+
* never collapses a meaningful matrix. */
|
|
1248
|
+
const IDENTITY_EPSILON = 1e-9;
|
|
1249
|
+
/** A `cmath.Transform` (`[[a,c,e],[b,d,f]]`) as a `matrix` op
|
|
1250
|
+
* (`matrix(a b c d e f)` argument order). */
|
|
1251
|
+
function transform_to_matrix_op(m) {
|
|
1252
|
+
return {
|
|
1253
|
+
type: "matrix",
|
|
1254
|
+
a: m[0][0],
|
|
1255
|
+
b: m[1][0],
|
|
1256
|
+
c: m[0][1],
|
|
1257
|
+
d: m[1][1],
|
|
1258
|
+
e: m[0][2],
|
|
1259
|
+
f: m[1][2]
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function is_identity_matrix(m) {
|
|
1263
|
+
const id = cmath.transform.identity;
|
|
1264
|
+
return Math.abs(m[0][0] - id[0][0]) <= IDENTITY_EPSILON && Math.abs(m[0][1] - id[0][1]) <= IDENTITY_EPSILON && Math.abs(m[0][2] - id[0][2]) <= IDENTITY_EPSILON && Math.abs(m[1][0] - id[1][0]) <= IDENTITY_EPSILON && Math.abs(m[1][1] - id[1][1]) <= IDENTITY_EPSILON && Math.abs(m[1][2] - id[1][2]) <= IDENTITY_EPSILON;
|
|
1265
|
+
}
|
|
1266
|
+
function apply_affine(transform_str, effective) {
|
|
1267
|
+
const ops = parse(transform_str);
|
|
1268
|
+
if (ops === null) return transform_str;
|
|
1269
|
+
const has_leading_matrix = ops.length > 0 && ops[0].type === "matrix";
|
|
1270
|
+
const existing_leading = has_leading_matrix ? op_matrix(ops[0]) : cmath.transform.identity;
|
|
1271
|
+
const rest = has_leading_matrix ? ops.slice(1) : ops;
|
|
1272
|
+
const folded = cmath.transform.multiply(effective, existing_leading);
|
|
1273
|
+
if (is_identity_matrix(folded)) {
|
|
1274
|
+
if (rest.length === 0) return null;
|
|
1275
|
+
return emit(rest);
|
|
1276
|
+
}
|
|
1277
|
+
return emit([transform_to_matrix_op(folded), ...rest]);
|
|
1278
|
+
}
|
|
1279
|
+
_transform.apply_affine = apply_affine;
|
|
335
1280
|
})(transform || (transform = {}));
|
|
336
1281
|
//#endregion
|
|
1282
|
+
//#region src/core/group.ts
|
|
1283
|
+
let group;
|
|
1284
|
+
(function(_group) {
|
|
1285
|
+
const STRUCTURAL_GRAPHICS = _group.STRUCTURAL_GRAPHICS = new Set([
|
|
1286
|
+
"g",
|
|
1287
|
+
"defs",
|
|
1288
|
+
"svg",
|
|
1289
|
+
"use",
|
|
1290
|
+
"image",
|
|
1291
|
+
"switch",
|
|
1292
|
+
"foreignObject",
|
|
1293
|
+
"path",
|
|
1294
|
+
"rect",
|
|
1295
|
+
"circle",
|
|
1296
|
+
"ellipse",
|
|
1297
|
+
"line",
|
|
1298
|
+
"polyline",
|
|
1299
|
+
"polygon",
|
|
1300
|
+
"text",
|
|
1301
|
+
"a"
|
|
1302
|
+
]);
|
|
1303
|
+
const CONSTRAINED_PARENT = _group.CONSTRAINED_PARENT = new Set([
|
|
1304
|
+
"text",
|
|
1305
|
+
"tspan",
|
|
1306
|
+
"defs",
|
|
1307
|
+
"clipPath",
|
|
1308
|
+
"mask",
|
|
1309
|
+
"pattern",
|
|
1310
|
+
"marker",
|
|
1311
|
+
"symbol",
|
|
1312
|
+
"filter",
|
|
1313
|
+
"linearGradient",
|
|
1314
|
+
"radialGradient",
|
|
1315
|
+
"animateMotion",
|
|
1316
|
+
"switch"
|
|
1317
|
+
]);
|
|
1318
|
+
function plan(doc, ids) {
|
|
1319
|
+
if (ids.length === 0) return null;
|
|
1320
|
+
const parent = doc.parent_of(ids[0]);
|
|
1321
|
+
if (parent === null) return null;
|
|
1322
|
+
for (const id of ids) {
|
|
1323
|
+
if (doc.parent_of(id) !== parent) return null;
|
|
1324
|
+
if (!STRUCTURAL_GRAPHICS.has(doc.tag_of(id))) return null;
|
|
1325
|
+
}
|
|
1326
|
+
if (CONSTRAINED_PARENT.has(doc.tag_of(parent))) return null;
|
|
1327
|
+
const siblings = doc.element_children_of(parent);
|
|
1328
|
+
const sibling_index = /* @__PURE__ */ new Map();
|
|
1329
|
+
for (let i = 0; i < siblings.length; i++) sibling_index.set(siblings[i], i);
|
|
1330
|
+
const indices = [];
|
|
1331
|
+
for (const id of ids) {
|
|
1332
|
+
const i = sibling_index.get(id);
|
|
1333
|
+
if (i === void 0) return null;
|
|
1334
|
+
indices.push(i);
|
|
1335
|
+
}
|
|
1336
|
+
const sorted = Array.from(new Set(indices)).sort((a, b) => a - b);
|
|
1337
|
+
for (let i = 1; i < sorted.length; i++) if (sorted[i] !== sorted[i - 1] + 1) return null;
|
|
1338
|
+
const children = sorted.map((i) => siblings[i]);
|
|
1339
|
+
const last_index = sorted[sorted.length - 1];
|
|
1340
|
+
const original_positions = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const i of sorted) original_positions.set(siblings[i], siblings[i + 1] ?? null);
|
|
1342
|
+
return {
|
|
1343
|
+
parent,
|
|
1344
|
+
insert_before: siblings[last_index + 1] ?? null,
|
|
1345
|
+
children,
|
|
1346
|
+
original_positions
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
_group.plan = plan;
|
|
1350
|
+
/**
|
|
1351
|
+
* Own-attribute allowlist for the safe clean-structural ungroup subset.
|
|
1352
|
+
* A group carrying ONLY these attributes is a plain structural wrapper:
|
|
1353
|
+
* dissolving it splices its children into the parent without changing
|
|
1354
|
+
* what renders (modulo baking the `transform`, which `ungroup` does).
|
|
1355
|
+
*
|
|
1356
|
+
* - `transform` — baked into each child by prepending its ops.
|
|
1357
|
+
* - `id` — only allowed when no `<use>` references it (checked below);
|
|
1358
|
+
* the group's own `id` simply disappears (no child inherits it).
|
|
1359
|
+
* - `data-grida-id` — the editor's runtime node identity. Internal; the
|
|
1360
|
+
* group node is removed entirely so the id retires with it.
|
|
1361
|
+
*
|
|
1362
|
+
* ANY other own attribute (`class`, `style`, `opacity`, `fill`,
|
|
1363
|
+
* `stroke`, `filter`, `clip-path`, `mask`, `font-*`, …) carries visual
|
|
1364
|
+
* / cascade / inheritance state that is NOT generally equivalent to the
|
|
1365
|
+
* per-child result of removing the group (TODO §10). Those groups are
|
|
1366
|
+
* refused — never silently mishandled.
|
|
1367
|
+
*/
|
|
1368
|
+
const UNGROUP_OWN_ATTR_ALLOWLIST = new Set([
|
|
1369
|
+
"transform",
|
|
1370
|
+
"id",
|
|
1371
|
+
"data-grida-id"
|
|
1372
|
+
]);
|
|
1373
|
+
/** SVG animation elements (SMIL). A direct animation child targets the
|
|
1374
|
+
* group as its `targetElement`; dissolving the group orphans the
|
|
1375
|
+
* animation. Refuse rather than relocate it. */
|
|
1376
|
+
const ANIMATION_TAGS = new Set([
|
|
1377
|
+
"animate",
|
|
1378
|
+
"animateTransform",
|
|
1379
|
+
"animateMotion",
|
|
1380
|
+
"set"
|
|
1381
|
+
]);
|
|
1382
|
+
function plan_ungroup(doc, id) {
|
|
1383
|
+
if (doc.tag_of(id) !== "g") return null;
|
|
1384
|
+
const parent = doc.parent_of(id);
|
|
1385
|
+
if (parent === null) return null;
|
|
1386
|
+
{
|
|
1387
|
+
let cur = parent;
|
|
1388
|
+
while (cur !== null) {
|
|
1389
|
+
if (doc.tag_of(cur) === "defs") return null;
|
|
1390
|
+
cur = doc.parent_of(cur);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const children = doc.element_children_of(id);
|
|
1394
|
+
if (children.length < 1) return null;
|
|
1395
|
+
let has_id = false;
|
|
1396
|
+
for (const a of doc.attributes_of(id)) {
|
|
1397
|
+
if (a.ns !== null) return null;
|
|
1398
|
+
if (!UNGROUP_OWN_ATTR_ALLOWLIST.has(a.name)) return null;
|
|
1399
|
+
if (a.name === "id") has_id = true;
|
|
1400
|
+
}
|
|
1401
|
+
if (has_id) {
|
|
1402
|
+
const own_id = doc.get_attr(id, "id");
|
|
1403
|
+
if (own_id !== null) {
|
|
1404
|
+
const fragment = `#${own_id}`;
|
|
1405
|
+
for (const use_id of doc.find_by_tag(doc.root, "use")) if ((doc.get_attr(use_id, "href") ?? doc.get_attr(use_id, "href", XLINK_NS$1)) === fragment) return null;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
for (const child of children) if (ANIMATION_TAGS.has(doc.tag_of(child))) return null;
|
|
1409
|
+
const group_transform = doc.get_attr(id, "transform");
|
|
1410
|
+
if (group_transform !== null) {
|
|
1411
|
+
if (transform.parse(group_transform) === null) return null;
|
|
1412
|
+
for (const child of children) if (transform.parse(doc.get_attr(child, "transform")) === null) return null;
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
group_id: id,
|
|
1416
|
+
parent,
|
|
1417
|
+
children: [...children],
|
|
1418
|
+
group_transform
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
_group.plan_ungroup = plan_ungroup;
|
|
1422
|
+
})(group || (group = {}));
|
|
1423
|
+
//#endregion
|
|
337
1424
|
//#region src/core/translate-pipeline/translate-pipeline.ts
|
|
338
1425
|
let translate_pipeline;
|
|
339
1426
|
(function(_translate_pipeline) {
|
|
@@ -935,6 +2022,27 @@ let rotate_pipeline;
|
|
|
935
2022
|
return { kind: "yes" };
|
|
936
2023
|
}
|
|
937
2024
|
_intent.is_rotatable = is_rotatable;
|
|
2025
|
+
function is_transformable(doc, id) {
|
|
2026
|
+
const own_transform = doc.get_attr(id, "transform");
|
|
2027
|
+
if (transform.parse(own_transform) === null) return {
|
|
2028
|
+
kind: "refuse",
|
|
2029
|
+
reason: "non-trivial-transform"
|
|
2030
|
+
};
|
|
2031
|
+
if (doc.has_glyph_rotate(id)) return {
|
|
2032
|
+
kind: "refuse",
|
|
2033
|
+
reason: "text-with-glyph-rotate"
|
|
2034
|
+
};
|
|
2035
|
+
if (doc.has_inline_css_transform(id)) return {
|
|
2036
|
+
kind: "refuse",
|
|
2037
|
+
reason: "css-property-transform"
|
|
2038
|
+
};
|
|
2039
|
+
if (doc.has_animate_transform_child(id)) return {
|
|
2040
|
+
kind: "refuse",
|
|
2041
|
+
reason: "animated-transform"
|
|
2042
|
+
};
|
|
2043
|
+
return { kind: "yes" };
|
|
2044
|
+
}
|
|
2045
|
+
_intent.is_transformable = is_transformable;
|
|
938
2046
|
function find_op(ops, type) {
|
|
939
2047
|
for (const op of ops) if (op.type === type) return op;
|
|
940
2048
|
return null;
|
|
@@ -3726,4 +4834,4 @@ function emitWithVerbs(network, meta) {
|
|
|
3726
4834
|
return encodeSVGPath(commands);
|
|
3727
4835
|
}
|
|
3728
4836
|
//#endregion
|
|
3729
|
-
export {
|
|
4837
|
+
export { XLINK_NS$1 as _, paint as a, is_text_input_focused as b, hit_shape_svg as c, NudgeDwellWatcher as d, TranslateOrchestrator as f, SvgDocument as g, transform as h, TOOL_CURSOR as i, RotateOrchestrator as l, group as m, insertions as n, ResizeOrchestrator as o, translate_pipeline as p, DEFAULT_STYLE as r, resize_pipeline as s, PathModel as t, rotate_pipeline as u, XMLNS_NS as v, array_shallow_equal as y };
|