@rasamjs/core 0.1.2 → 0.3.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/API.md +29 -9
- package/CHANGELOG.md +98 -1
- package/COMPONENTS.md +229 -0
- package/README.md +178 -25
- package/RELEASING.md +31 -21
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/catalog/fragment.d.ts +2 -2
- package/dist/catalog/fragment.d.ts.map +1 -1
- package/dist/catalog/fragment.js.map +1 -1
- package/dist/changes/derived.d.ts +10 -0
- package/dist/changes/derived.d.ts.map +1 -0
- package/dist/changes/derived.js +14 -0
- package/dist/changes/derived.js.map +1 -0
- package/dist/changes/restore.d.ts.map +1 -1
- package/dist/changes/restore.js +22 -4
- package/dist/changes/restore.js.map +1 -1
- package/dist/changes/service.d.ts +10 -2
- package/dist/changes/service.d.ts.map +1 -1
- package/dist/changes/service.js +45 -6
- package/dist/changes/service.js.map +1 -1
- package/dist/changes/types.d.ts +9 -0
- package/dist/changes/types.d.ts.map +1 -1
- package/dist/changes/types.js.map +1 -1
- package/dist/changes.d.ts +5 -3
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +103 -2
- package/dist/changes.js.map +1 -1
- package/dist/components/changes.d.ts +34 -0
- package/dist/components/changes.d.ts.map +1 -0
- package/dist/components/changes.js +119 -0
- package/dist/components/changes.js.map +1 -0
- package/dist/components/component.d.ts +62 -0
- package/dist/components/component.d.ts.map +1 -0
- package/dist/components/component.js +355 -0
- package/dist/components/component.js.map +1 -0
- package/dist/components/decorators.d.ts +7 -0
- package/dist/components/decorators.d.ts.map +1 -0
- package/dist/components/decorators.js +81 -0
- package/dist/components/decorators.js.map +1 -0
- package/dist/components/metadata.d.ts +10 -0
- package/dist/components/metadata.d.ts.map +1 -0
- package/dist/components/metadata.js +85 -0
- package/dist/components/metadata.js.map +1 -0
- package/dist/components/registry.d.ts +24 -0
- package/dist/components/registry.d.ts.map +1 -0
- package/dist/components/registry.js +78 -0
- package/dist/components/registry.js.map +1 -0
- package/dist/components/types.d.ts +54 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +2 -0
- package/dist/components/types.js.map +1 -0
- package/dist/components/view.d.ts +9 -0
- package/dist/components/view.d.ts.map +1 -0
- package/dist/components/view.js +104 -0
- package/dist/components/view.js.map +1 -0
- package/dist/components.d.ts +6 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +5 -0
- package/dist/components.js.map +1 -0
- package/dist/core/node.d.ts +7 -0
- package/dist/core/node.d.ts.map +1 -1
- package/dist/core/node.js +30 -0
- package/dist/core/node.js.map +1 -1
- package/dist/dom/images.d.ts.map +1 -1
- package/dist/dom/images.js +3 -1
- package/dist/dom/images.js.map +1 -1
- package/dist/elements/image.d.ts.map +1 -1
- package/dist/elements/image.js +4 -1
- package/dist/elements/image.js.map +1 -1
- package/dist/interchange/clone.d.ts.map +1 -1
- package/dist/interchange/clone.js +4 -3
- package/dist/interchange/clone.js.map +1 -1
- package/dist/interchange/dom.d.ts +3 -1
- package/dist/interchange/dom.d.ts.map +1 -1
- package/dist/interchange/dom.js +29 -5
- package/dist/interchange/dom.js.map +1 -1
- package/dist/interchange/markup.d.ts.map +1 -1
- package/dist/interchange/markup.js +71 -3
- package/dist/interchange/markup.js.map +1 -1
- package/dist/interchange/policy.d.ts.map +1 -1
- package/dist/interchange/policy.js +4 -0
- package/dist/interchange/policy.js.map +1 -1
- package/dist/interchange/reconstruct.d.ts.map +1 -1
- package/dist/interchange/reconstruct.js +16 -12
- package/dist/interchange/reconstruct.js.map +1 -1
- package/dist/interchange/references.d.ts.map +1 -1
- package/dist/interchange/references.js +10 -4
- package/dist/interchange/references.js.map +1 -1
- package/dist/interchange/types.d.ts +10 -1
- package/dist/interchange/types.d.ts.map +1 -1
- package/dist/mixins/parent-internals.d.ts +6 -0
- package/dist/mixins/parent-internals.d.ts.map +1 -1
- package/dist/mixins/parent-internals.js +34 -0
- package/dist/mixins/parent-internals.js.map +1 -1
- package/dist/mixins/parent.d.ts.map +1 -1
- package/dist/mixins/parent.js +6 -1
- package/dist/mixins/parent.js.map +1 -1
- package/dist/mixins/transformable.d.ts.map +1 -1
- package/dist/mixins/transformable.js +25 -27
- package/dist/mixins/transformable.js.map +1 -1
- package/dist/mixins/types.d.ts +2 -2
- package/dist/mixins/types.d.ts.map +1 -1
- package/dist/raster/frame.d.ts +12 -0
- package/dist/raster/frame.d.ts.map +1 -0
- package/dist/raster/frame.js +43 -0
- package/dist/raster/frame.js.map +1 -0
- package/dist/raster/images.d.ts +2 -0
- package/dist/raster/images.d.ts.map +1 -0
- package/dist/raster/images.js +60 -0
- package/dist/raster/images.js.map +1 -0
- package/dist/raster/render.d.ts +3 -0
- package/dist/raster/render.d.ts.map +1 -0
- package/dist/raster/render.js +40 -0
- package/dist/raster/render.js.map +1 -0
- package/dist/raster/types.d.ts +10 -0
- package/dist/raster/types.d.ts.map +1 -0
- package/dist/raster/types.js +2 -0
- package/dist/raster/types.js.map +1 -0
- package/dist/raster.d.ts +6 -0
- package/dist/raster.d.ts.map +1 -0
- package/dist/raster.js +10 -0
- package/dist/raster.js.map +1 -0
- package/dist/resources/references.d.ts +2 -0
- package/dist/resources/references.d.ts.map +1 -1
- package/dist/resources/references.js +3 -1
- package/dist/resources/references.js.map +1 -1
- package/dist/services/catalog.d.ts.map +1 -1
- package/dist/services/catalog.js +5 -3
- package/dist/services/catalog.js.map +1 -1
- package/dist/services/defs-manager.d.ts +2 -0
- package/dist/services/defs-manager.d.ts.map +1 -1
- package/dist/services/defs-manager.js +4 -0
- package/dist/services/defs-manager.js.map +1 -1
- package/dist/services/element-key.d.ts +4 -0
- package/dist/services/element-key.d.ts.map +1 -0
- package/dist/services/element-key.js +12 -0
- package/dist/services/element-key.js.map +1 -0
- package/dist/services/initialization.d.ts +7 -0
- package/dist/services/initialization.d.ts.map +1 -0
- package/dist/services/initialization.js +27 -0
- package/dist/services/initialization.js.map +1 -0
- package/dist/services/registry.d.ts +8 -0
- package/dist/services/registry.d.ts.map +1 -1
- package/dist/services/registry.js +45 -10
- package/dist/services/registry.js.map +1 -1
- package/dist/services/types.d.ts +17 -0
- package/dist/services/types.d.ts.map +1 -1
- package/dist/services/viewport.d.ts +2 -0
- package/dist/services/viewport.d.ts.map +1 -1
- package/dist/services/viewport.js +4 -3
- package/dist/services/viewport.js.map +1 -1
- package/dist/template/parts.d.ts +12 -6
- package/dist/template/parts.d.ts.map +1 -1
- package/dist/template/parts.js +36 -29
- package/dist/template/parts.js.map +1 -1
- package/dist/template/render.d.ts +6 -7
- package/dist/template/render.d.ts.map +1 -1
- package/dist/template/render.js +188 -44
- package/dist/template/render.js.map +1 -1
- package/dist/template/svg-tag.d.ts +5 -1
- package/dist/template/svg-tag.d.ts.map +1 -1
- package/dist/template/svg-tag.js.map +1 -1
- package/dist/text/content.d.ts +0 -2
- package/dist/text/content.d.ts.map +1 -1
- package/dist/text/content.js +4 -10
- package/dist/text/content.js.map +1 -1
- package/dist/text/internals.d.ts +1 -0
- package/dist/text/internals.d.ts.map +1 -1
- package/dist/text/policies.d.ts +7 -0
- package/dist/text/policies.d.ts.map +1 -0
- package/dist/text/policies.js +13 -0
- package/dist/text/policies.js.map +1 -0
- package/dist/types/common.d.ts +27 -0
- package/dist/types/common.d.ts.map +1 -1
- package/package.json +156 -143
- package/reports/adoption.json +20 -20
- package/reports/animation-benchmark.json +616 -616
- package/reports/benchmark.json +65 -65
- package/reports/components-benchmark-before.json +10 -0
- package/reports/components-benchmark.json +10 -0
- package/reports/creation.json +26 -26
- package/reports/interchange-benchmark.json +85 -85
- package/reports/public-api-contract.json +353 -23
- package/reports/public-api-review.md +23 -0
- package/reports/size-components.json +102 -0
- package/reports/size-parity.json +82 -34
- package/reports/size.json +6 -4
- package/reports/svgjs-comparison.json +3466 -3466
- package/reports/svgjs-comparison.md +19 -19
- package/reports/svgjs-parity-current.json +177 -60
- package/src/advanced.ts +1 -1
- package/src/catalog/fragment.ts +2 -2
- package/src/changes/derived.ts +19 -0
- package/src/changes/restore.ts +27 -4
- package/src/changes/service.ts +60 -11
- package/src/changes/types.ts +9 -0
- package/src/changes.ts +93 -4
- package/src/components/changes.ts +131 -0
- package/src/components/component.ts +401 -0
- package/src/components/decorators.ts +104 -0
- package/src/components/metadata.ts +111 -0
- package/src/components/registry.ts +79 -0
- package/src/components/types.ts +66 -0
- package/src/components/view.ts +123 -0
- package/src/components.ts +15 -0
- package/src/core/node.ts +32 -1
- package/src/dom/images.ts +2 -1
- package/src/elements/image.ts +4 -1
- package/src/interchange/clone.ts +4 -3
- package/src/interchange/dom.ts +34 -5
- package/src/interchange/markup.ts +77 -3
- package/src/interchange/policy.ts +6 -0
- package/src/interchange/reconstruct.ts +16 -12
- package/src/interchange/references.ts +12 -4
- package/src/interchange/types.ts +10 -1
- package/src/mixins/parent-internals.ts +37 -0
- package/src/mixins/parent.ts +13 -4
- package/src/mixins/transformable.ts +23 -25
- package/src/mixins/types.ts +2 -2
- package/src/raster/frame.ts +53 -0
- package/src/raster/images.ts +73 -0
- package/src/raster/render.ts +45 -0
- package/src/raster/types.ts +9 -0
- package/src/raster.ts +13 -0
- package/src/resources/references.ts +5 -1
- package/src/services/catalog.ts +5 -3
- package/src/services/defs-manager.ts +5 -0
- package/src/services/element-key.ts +14 -0
- package/src/services/initialization.ts +25 -0
- package/src/services/registry.ts +47 -10
- package/src/services/types.ts +21 -0
- package/src/services/viewport.ts +5 -4
- package/src/template/parts.ts +54 -39
- package/src/template/render.ts +211 -54
- package/src/template/svg-tag.ts +6 -1
- package/src/text/content.ts +4 -14
- package/src/text/internals.ts +1 -0
- package/src/text/policies.ts +18 -0
- package/src/types/common.ts +29 -0
|
@@ -4,11 +4,48 @@ import { assertNotDestroying, pendingDestruction } from '../core/destruction.js'
|
|
|
4
4
|
/** Structural preflight shared by Parent and the opt-in tree operations. */
|
|
5
5
|
export const validateChild = Symbol('validateChild');
|
|
6
6
|
|
|
7
|
+
let internalStructure = 0;
|
|
8
|
+
const unlocked = new Set<AnyNode>();
|
|
9
|
+
|
|
10
|
+
/** @internal Core's own exact-tree paths (adoption, replay, rollback) bypass child ownership. */
|
|
11
|
+
export function withInternalStructure<Result>(callback: () => Result): Result {
|
|
12
|
+
internalStructure++;
|
|
13
|
+
try {
|
|
14
|
+
return callback();
|
|
15
|
+
} finally {
|
|
16
|
+
internalStructure--;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @internal Lets a node whose class owns its children change them inside the callback. */
|
|
21
|
+
export function withOwnedChildren<Result>(parent: AnyNode, callback: () => Result): Result {
|
|
22
|
+
if (unlocked.has(parent)) return callback();
|
|
23
|
+
unlocked.add(parent);
|
|
24
|
+
try {
|
|
25
|
+
return callback();
|
|
26
|
+
} finally {
|
|
27
|
+
unlocked.delete(parent);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Throws for outside structural edits of a node whose class owns its children. */
|
|
32
|
+
export function assertChildrenEditable(parent: AnyNode | null | undefined): void {
|
|
33
|
+
if (
|
|
34
|
+
parent &&
|
|
35
|
+
(parent.constructor as { readonly ownsChildren?: boolean }).ownsChildren &&
|
|
36
|
+
!internalStructure &&
|
|
37
|
+
!unlocked.has(parent) &&
|
|
38
|
+
!pendingDestruction.has(parent)
|
|
39
|
+
)
|
|
40
|
+
throw new Error('This node owns its children');
|
|
41
|
+
}
|
|
42
|
+
|
|
7
43
|
export function assertCanAddChildren(parent: AnyNode): void {
|
|
8
44
|
parent.assertLive();
|
|
9
45
|
// Whole-drawing teardown already cleans late subscriptions and registrations.
|
|
10
46
|
const drawingCloses = parent.context.root && pendingDestruction.has(parent.context.root);
|
|
11
47
|
if (!drawingCloses) assertNotDestroying(parent);
|
|
48
|
+
assertChildrenEditable(parent);
|
|
12
49
|
}
|
|
13
50
|
|
|
14
51
|
export function validateIndex(index: number, length: number): void {
|
package/src/mixins/parent.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { mutation } from '../changes/mutation.js';
|
|
2
2
|
import type { Node } from '../core/node.js';
|
|
3
3
|
import type { AnyNode } from '../core/types.js';
|
|
4
|
-
import type { ElementClass } from '../services/types.js';
|
|
4
|
+
import type { ElementClass, CreationOptions } from '../services/types.js';
|
|
5
5
|
import { BBox } from '../math/bbox.js';
|
|
6
6
|
import type { Bounds } from '../types/common.js';
|
|
7
7
|
import type { MixinParent, NodeSelector } from './types.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assertCanAddChildren,
|
|
10
|
+
assertChildrenEditable,
|
|
11
|
+
validateChild,
|
|
12
|
+
validateIndex,
|
|
13
|
+
} from './parent-internals.js';
|
|
9
14
|
import { assertNotDestroying } from '../core/destruction.js';
|
|
10
15
|
|
|
11
16
|
export const Parent: MixinParent = (BaseClass) => {
|
|
@@ -50,6 +55,7 @@ export const Parent: MixinParent = (BaseClass) => {
|
|
|
50
55
|
adopt(element: SVGElement): AnyNode {
|
|
51
56
|
return mutation(this, () => {
|
|
52
57
|
this.assertLive();
|
|
58
|
+
assertChildrenEditable(this);
|
|
53
59
|
const siblings = this.context.dom.children(this.el);
|
|
54
60
|
if (!siblings.includes(element))
|
|
55
61
|
throw new Error('Element is not a native child of this parent');
|
|
@@ -93,6 +99,7 @@ export const Parent: MixinParent = (BaseClass) => {
|
|
|
93
99
|
[validateChild](child: AnyNode): void {
|
|
94
100
|
assertCanAddChildren(this);
|
|
95
101
|
child.assertLive();
|
|
102
|
+
if (child.parent !== this) assertChildrenEditable(child.parent);
|
|
96
103
|
assertNotDestroying(child);
|
|
97
104
|
this.validateChildTag(this.context.dom.tag(child.el));
|
|
98
105
|
if (child.context !== this.context) throw new Error('Cross-root moves are unsupported');
|
|
@@ -110,13 +117,13 @@ export const Parent: MixinParent = (BaseClass) => {
|
|
|
110
117
|
|
|
111
118
|
create<Options, Child extends AnyNode>(
|
|
112
119
|
Constructor: ElementClass<Options, Child>,
|
|
113
|
-
options: Options,
|
|
120
|
+
options: NoInfer<CreationOptions<Options, Child>>,
|
|
114
121
|
): Child {
|
|
115
122
|
return mutation(this, () => {
|
|
116
123
|
assertCanAddChildren(this);
|
|
117
124
|
this.validateChildTag(Constructor.tag);
|
|
118
125
|
this.context.registry.register(Constructor);
|
|
119
|
-
return this.add(new Constructor(this.context, options));
|
|
126
|
+
return this.add(new Constructor(this.context, options as Options));
|
|
120
127
|
});
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -128,6 +135,7 @@ export const Parent: MixinParent = (BaseClass) => {
|
|
|
128
135
|
|
|
129
136
|
detachChild(child: AnyNode): void {
|
|
130
137
|
return mutation(this, () => {
|
|
138
|
+
assertChildrenEditable(this);
|
|
131
139
|
const index = this.#children.indexOf(child);
|
|
132
140
|
if (index >= 0) {
|
|
133
141
|
this.context.changes.touch(child);
|
|
@@ -139,6 +147,7 @@ export const Parent: MixinParent = (BaseClass) => {
|
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
private childrenChanged(): void {
|
|
150
|
+
this.context.changes.invalidateOrder(this);
|
|
142
151
|
this.context.bounds.invalidate();
|
|
143
152
|
this.context.defs.invalidateHost(this);
|
|
144
153
|
}
|
|
@@ -53,8 +53,8 @@ export const Transformable: MixinTransformable = (BaseClass) => {
|
|
|
53
53
|
matrix(): Matrix;
|
|
54
54
|
matrix(value: Matrix | MatrixValues): this;
|
|
55
55
|
matrix(value?: Matrix | MatrixValues): Matrix | this {
|
|
56
|
+
if (value === undefined) return this.localMatrix();
|
|
56
57
|
return mutation(this, () => {
|
|
57
|
-
if (value === undefined) return this.localMatrix();
|
|
58
58
|
this.assertLive();
|
|
59
59
|
const matrix = Matrix.from(value);
|
|
60
60
|
this.#matrixMode = true;
|
|
@@ -110,33 +110,31 @@ export const Transformable: MixinTransformable = (BaseClass) => {
|
|
|
110
110
|
transformState(): TransformState;
|
|
111
111
|
transformState(state: TransformState): this;
|
|
112
112
|
transformState(state?: TransformState): TransformState | this {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (this.#matrixMode || transform !== this.#lastTransform)
|
|
125
|
-
return Object.freeze({
|
|
126
|
-
...common,
|
|
127
|
-
mode: 'matrix',
|
|
128
|
-
matrix: Object.freeze(base.toArray()),
|
|
129
|
-
});
|
|
130
|
-
const components = this.components;
|
|
113
|
+
this.assertLive();
|
|
114
|
+
if (state !== undefined) return mutation(this, () => this.restoreTransformState(state));
|
|
115
|
+
const { base, geometry } = this.mappingState();
|
|
116
|
+
const transform = this.getAttributeValue('transform');
|
|
117
|
+
const common = {
|
|
118
|
+
tag: this.context.dom.tag(this.el),
|
|
119
|
+
transform: transform === undefined ? null : String(transform),
|
|
120
|
+
geometry: Object.freeze(geometry.toArray()),
|
|
121
|
+
attributes: geometrySnapshot(this),
|
|
122
|
+
};
|
|
123
|
+
if (this.#matrixMode || transform !== this.#lastTransform)
|
|
131
124
|
return Object.freeze({
|
|
132
125
|
...common,
|
|
133
|
-
mode: '
|
|
134
|
-
|
|
135
|
-
rotation: components.rotation,
|
|
136
|
-
scale: components.scale,
|
|
137
|
-
origin: components.origin,
|
|
138
|
-
automaticOrigin: !components.explicitOrigin,
|
|
126
|
+
mode: 'matrix',
|
|
127
|
+
matrix: Object.freeze(base.toArray()),
|
|
139
128
|
});
|
|
129
|
+
const components = this.components;
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
...common,
|
|
132
|
+
mode: 'components',
|
|
133
|
+
position: components.position,
|
|
134
|
+
rotation: components.rotation,
|
|
135
|
+
scale: components.scale,
|
|
136
|
+
origin: components.origin,
|
|
137
|
+
automaticOrigin: !components.explicitOrigin,
|
|
140
138
|
});
|
|
141
139
|
}
|
|
142
140
|
|
package/src/mixins/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { Text } from '../elements/text.js';
|
|
|
16
16
|
import type { Use } from '../elements/use.js';
|
|
17
17
|
import type { Matrix } from '../math/matrix.js';
|
|
18
18
|
import type { Vec2 } from '../math/vec2.js';
|
|
19
|
-
import type { ElementClass } from '../services/types.js';
|
|
19
|
+
import type { ElementClass, CreationOptions } from '../services/types.js';
|
|
20
20
|
import type {
|
|
21
21
|
CommonAttributes,
|
|
22
22
|
Bounds,
|
|
@@ -62,7 +62,7 @@ export interface ParentFeature {
|
|
|
62
62
|
detachChild(child: AnyNode): void;
|
|
63
63
|
create<Options, Child extends AnyNode>(
|
|
64
64
|
Constructor: ElementClass<Options, Child>,
|
|
65
|
-
options: Options,
|
|
65
|
+
options: NoInfer<CreationOptions<Options, Child>>,
|
|
66
66
|
): Child;
|
|
67
67
|
remove(): this;
|
|
68
68
|
remove(child: AnyNode): this;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AnyNode } from '../core/types.js';
|
|
2
|
+
import { exportSvg } from '../interchange/markup.js';
|
|
3
|
+
import type { RasterOptions } from './types.js';
|
|
4
|
+
export function prepareRaster(root: AnyNode, options: RasterOptions) {
|
|
5
|
+
root.assertLive();
|
|
6
|
+
const document = root.el.ownerDocument,
|
|
7
|
+
view = document.defaultView;
|
|
8
|
+
if (!view) throw new Error('Raster export needs a browser document');
|
|
9
|
+
const type = options.type ?? 'image/png',
|
|
10
|
+
scale = options.scale ?? 1,
|
|
11
|
+
frame = options.frame ?? 'content',
|
|
12
|
+
padding = options.padding ?? 0;
|
|
13
|
+
if (!['image/png', 'image/webp', 'image/jpeg'].includes(type))
|
|
14
|
+
throw new TypeError('Unknown raster image type');
|
|
15
|
+
if (!Number.isFinite(scale) || scale <= 0)
|
|
16
|
+
throw new RangeError('Raster scale must be positive and finite');
|
|
17
|
+
if (!Number.isFinite(padding) || padding < 0)
|
|
18
|
+
throw new RangeError('Raster padding must be nonnegative and finite');
|
|
19
|
+
if (!['content', 'viewBox'].includes(frame)) throw new TypeError('Unknown raster frame');
|
|
20
|
+
const background = options.background ?? (type === 'image/jpeg' ? '#ffffff' : undefined);
|
|
21
|
+
if (
|
|
22
|
+
background !== undefined &&
|
|
23
|
+
(typeof background !== 'string' || !view.CSS.supports('color', background))
|
|
24
|
+
)
|
|
25
|
+
throw new TypeError('Raster background must be a CSS color');
|
|
26
|
+
const markup = exportSvg(root, { frame: frame === 'content' ? 'content' : undefined, padding });
|
|
27
|
+
const svg = new view.DOMParser().parseFromString(markup, 'image/svg+xml').documentElement;
|
|
28
|
+
const box = svg
|
|
29
|
+
.getAttribute('viewBox')
|
|
30
|
+
?.trim()
|
|
31
|
+
.split(/[\s,]+/)
|
|
32
|
+
.map(Number);
|
|
33
|
+
const extent = root.context.root!.el.getBoundingClientRect();
|
|
34
|
+
const length = (name: 'width' | 'height', fallback: number) => {
|
|
35
|
+
const value = svg.getAttribute(name) ?? '';
|
|
36
|
+
return /^\d+(?:\.\d+)?(?:px)?$/.test(value) ? parseFloat(value) : fallback;
|
|
37
|
+
};
|
|
38
|
+
const width = box?.length === 4 ? box[2] : length('width', extent.width),
|
|
39
|
+
height = box?.length === 4 ? box[3] : length('height', extent.height);
|
|
40
|
+
const pixels = [Math.ceil(width * scale), Math.ceil(height * scale)];
|
|
41
|
+
if (
|
|
42
|
+
!pixels.every((value) => Number.isSafeInteger(value) && value > 0 && value <= 32767) ||
|
|
43
|
+
pixels[0] * pixels[1] > 16777216
|
|
44
|
+
)
|
|
45
|
+
throw new RangeError(
|
|
46
|
+
'Raster export needs positive dimensions, at most 32767 per axis and 16777216 pixels; reduce scale or add padding to zero-size content',
|
|
47
|
+
);
|
|
48
|
+
svg.setAttribute('width', String(pixels[0]));
|
|
49
|
+
svg.setAttribute('height', String(pixels[1]));
|
|
50
|
+
if (!box) svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
51
|
+
return { document, svg, type, background, width: pixels[0], height: pixels[1] };
|
|
52
|
+
}
|
|
53
|
+
export type RasterSnapshot = ReturnType<typeof prepareRaster>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Inline supported image resources; SVG-as-image rendering cannot load external files. */
|
|
2
|
+
const xlink = 'http://www.w3.org/1999/xlink';
|
|
3
|
+
export async function embedImages(
|
|
4
|
+
svg: Element,
|
|
5
|
+
document: Document,
|
|
6
|
+
base: string,
|
|
7
|
+
ancestors: readonly string[] = [],
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const view = document.defaultView!;
|
|
10
|
+
for (const image of svg.querySelectorAll('image,feImage')) {
|
|
11
|
+
const href = image.getAttribute('href') ?? image.getAttributeNS(xlink, 'href');
|
|
12
|
+
if (!href || href.startsWith('#')) continue;
|
|
13
|
+
const url = new URL(href, base);
|
|
14
|
+
if (
|
|
15
|
+
url.protocol !== 'data:' &&
|
|
16
|
+
(!['http:', 'https:'].includes(url.protocol) || url.origin !== new URL(document.URL).origin)
|
|
17
|
+
)
|
|
18
|
+
throw new Error('Raster export supports only same-origin and data-URL images');
|
|
19
|
+
if (ancestors.length >= 8 || ancestors.includes(url.href))
|
|
20
|
+
throw new Error('Raster image resources are cyclic or too deeply nested');
|
|
21
|
+
let blob: Blob;
|
|
22
|
+
try {
|
|
23
|
+
const response = await view.fetch(
|
|
24
|
+
url.href,
|
|
25
|
+
url.protocol === 'data:'
|
|
26
|
+
? {}
|
|
27
|
+
: { mode: 'same-origin', credentials: 'same-origin', redirect: 'error' },
|
|
28
|
+
);
|
|
29
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
30
|
+
blob = await response.blob();
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
throw new Error('Cannot load a raster image resource', { cause });
|
|
33
|
+
}
|
|
34
|
+
const mime = blob.type.split(';')[0].toLowerCase();
|
|
35
|
+
if (
|
|
36
|
+
![
|
|
37
|
+
'image/png',
|
|
38
|
+
'image/jpeg',
|
|
39
|
+
'image/webp',
|
|
40
|
+
'image/gif',
|
|
41
|
+
'image/avif',
|
|
42
|
+
'image/svg+xml',
|
|
43
|
+
].includes(mime)
|
|
44
|
+
)
|
|
45
|
+
throw new Error('Unsupported raster image resource type');
|
|
46
|
+
if (mime === 'image/svg+xml') {
|
|
47
|
+
const nested = new view.DOMParser().parseFromString(await blob.text(), 'image/svg+xml');
|
|
48
|
+
if (nested.querySelector('parsererror') || nested.documentElement.localName !== 'svg')
|
|
49
|
+
throw new Error('Invalid SVG image resource');
|
|
50
|
+
await embedImages(
|
|
51
|
+
nested.documentElement,
|
|
52
|
+
document,
|
|
53
|
+
url.protocol === 'data:' ? base : url.href,
|
|
54
|
+
[...ancestors, url.href],
|
|
55
|
+
);
|
|
56
|
+
blob = new view.Blob([new view.XMLSerializer().serializeToString(nested)], { type: mime });
|
|
57
|
+
}
|
|
58
|
+
const data = await new Promise<string>((resolve, reject) => {
|
|
59
|
+
const reader = new view.FileReader();
|
|
60
|
+
reader.onload = () => resolve(String(reader.result));
|
|
61
|
+
reader.onerror = () => reject(new Error('Cannot encode a raster image resource'));
|
|
62
|
+
reader.readAsDataURL(blob);
|
|
63
|
+
});
|
|
64
|
+
await new Promise<void>((resolve, reject) => {
|
|
65
|
+
const decoded = new view.Image();
|
|
66
|
+
decoded.onload = () => resolve();
|
|
67
|
+
decoded.onerror = () => reject(new Error('Cannot decode a raster image resource'));
|
|
68
|
+
decoded.src = data;
|
|
69
|
+
});
|
|
70
|
+
image.removeAttributeNS(xlink, 'href');
|
|
71
|
+
image.setAttribute('href', data);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RasterSnapshot } from './frame.js';
|
|
2
|
+
export async function renderRaster({
|
|
3
|
+
document,
|
|
4
|
+
svg,
|
|
5
|
+
type,
|
|
6
|
+
background,
|
|
7
|
+
width,
|
|
8
|
+
height,
|
|
9
|
+
}: RasterSnapshot): Promise<Blob> {
|
|
10
|
+
const view = document.defaultView!;
|
|
11
|
+
const url = view.URL.createObjectURL(
|
|
12
|
+
new view.Blob([new view.XMLSerializer().serializeToString(svg)], { type: 'image/svg+xml' }),
|
|
13
|
+
);
|
|
14
|
+
try {
|
|
15
|
+
const image = new view.Image();
|
|
16
|
+
await new Promise<void>((resolve, reject) => {
|
|
17
|
+
image.onload = () => resolve();
|
|
18
|
+
image.onerror = () => reject(new Error('Cannot decode SVG for raster export'));
|
|
19
|
+
image.src = url;
|
|
20
|
+
});
|
|
21
|
+
const canvas = document.createElement('canvas');
|
|
22
|
+
canvas.width = width;
|
|
23
|
+
canvas.height = height;
|
|
24
|
+
const context = canvas.getContext('2d');
|
|
25
|
+
if (!context) throw new Error('Raster export needs a 2D canvas context');
|
|
26
|
+
if (background !== undefined) {
|
|
27
|
+
context.fillStyle = background;
|
|
28
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
29
|
+
}
|
|
30
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
31
|
+
return await new Promise<Blob>((resolve, reject) =>
|
|
32
|
+
canvas.toBlob((blob) => {
|
|
33
|
+
if (!blob) reject(new Error('Browser could not encode the raster image'));
|
|
34
|
+
else if (blob.type !== type) reject(new Error(`Browser cannot encode ${type}`));
|
|
35
|
+
else resolve(blob);
|
|
36
|
+
}, type),
|
|
37
|
+
);
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
if ((cause as { name?: string })?.name === 'SecurityError')
|
|
40
|
+
throw new Error('Cannot export image: canvas is tainted by cross-origin content', { cause });
|
|
41
|
+
throw cause;
|
|
42
|
+
} finally {
|
|
43
|
+
view.URL.revokeObjectURL(url);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface RasterOptions {
|
|
2
|
+
readonly type?: 'image/png' | 'image/webp' | 'image/jpeg';
|
|
3
|
+
readonly scale?: number;
|
|
4
|
+
readonly background?: string;
|
|
5
|
+
/** Content bounds by default; viewBox exports the current document camera. */
|
|
6
|
+
readonly frame?: 'content' | 'viewBox';
|
|
7
|
+
/** Drawing units around content bounds, default zero. */
|
|
8
|
+
readonly padding?: number;
|
|
9
|
+
}
|
package/src/raster.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnyNode } from './core/types.js';
|
|
2
|
+
import type { RasterOptions } from './raster/types.js';
|
|
3
|
+
import { prepareRaster } from './raster/frame.js';
|
|
4
|
+
import { embedImages } from './raster/images.js';
|
|
5
|
+
import { renderRaster } from './raster/render.js';
|
|
6
|
+
export type { RasterOptions };
|
|
7
|
+
|
|
8
|
+
/** Snapshot and render a passive SVG document without changing its camera or model. */
|
|
9
|
+
export async function exportImage(root: AnyNode, options: RasterOptions = {}): Promise<Blob> {
|
|
10
|
+
const snapshot = prepareRaster(root, options);
|
|
11
|
+
await embedImages(snapshot.svg, snapshot.document, root.el.baseURI);
|
|
12
|
+
return renderRaster(snapshot);
|
|
13
|
+
}
|
|
@@ -30,12 +30,16 @@ function localReference(
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Connector ends link to their targets; the targets are not rendering resources. */
|
|
34
|
+
export const isLink = (attribute: string): boolean =>
|
|
35
|
+
attribute === 'data-rasam-connector-start' || attribute === 'data-rasam-connector-end';
|
|
36
|
+
|
|
33
37
|
/** Classifies local fragments for lookup/remapping; it never acquires a typed binding. */
|
|
34
38
|
export function classifyReference(attribute: string, value: unknown): LocalReference | null {
|
|
35
39
|
attribute = Attributes.normalizeName(attribute);
|
|
36
40
|
if (typeof value !== 'string') return null;
|
|
37
41
|
const source = value.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
|
|
38
|
-
if (attribute === 'href') {
|
|
42
|
+
if (attribute === 'href' || isLink(attribute)) {
|
|
39
43
|
const match = /^#([^\s#<>"'()\\\u0000-\u001f\u007f]+)$/.exec(source);
|
|
40
44
|
return localReference('href', match?.[1]);
|
|
41
45
|
}
|
package/src/services/catalog.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Registry } from './registry.js';
|
|
2
|
+
import { elementKey } from './element-key.js';
|
|
2
3
|
import type { ElementClass } from './types.js';
|
|
3
4
|
|
|
4
5
|
/** Preflight a per-drawing catalog before changing the registry. */
|
|
5
6
|
export function registerElements(registry: Registry, elements: readonly ElementClass[]): void {
|
|
6
7
|
const pending = new Map<string, ElementClass>();
|
|
7
8
|
for (const Constructor of elements) {
|
|
8
|
-
const
|
|
9
|
+
const key = elementKey(Constructor);
|
|
10
|
+
const previous = pending.get(key) ?? registry.registered(Constructor);
|
|
9
11
|
if (previous && previous !== Constructor)
|
|
10
|
-
throw new Error(
|
|
11
|
-
pending.set(
|
|
12
|
+
throw new Error(`${Constructor.variant ? 'Variant' : 'Tag'} already registered: ${key}`);
|
|
13
|
+
pending.set(key, Constructor);
|
|
12
14
|
}
|
|
13
15
|
for (const Constructor of pending.values()) registry.register(Constructor);
|
|
14
16
|
}
|
|
@@ -56,6 +56,11 @@ export class DefsManager {
|
|
|
56
56
|
return count;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Nodes holding a typed reference to `target`, detached ones included, from the index. */
|
|
60
|
+
consumersOf(target: AnyNode): AnyNode[] {
|
|
61
|
+
return [...(this.consumersByTarget.get(target)?.keys() ?? [])];
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
*geometryConsumers(target: AnyNode): IterableIterator<AnyNode> {
|
|
60
65
|
for (const [consumer, attributes] of this.consumersByTarget.get(target) ?? [])
|
|
61
66
|
if (attributes.has('href')) yield consumer;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ElementClass } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** A tag, or `tag[attribute="value"]` for a variant. Variants share the constructor map. */
|
|
4
|
+
export function elementKey(Constructor: ElementClass): string {
|
|
5
|
+
const variant = Constructor.variant;
|
|
6
|
+
if (!variant) return Constructor.tag;
|
|
7
|
+
if (
|
|
8
|
+
!/^data-[a-z0-9_.-]+$/.test(variant.attribute) ||
|
|
9
|
+
typeof variant.value != 'string' ||
|
|
10
|
+
!variant.value
|
|
11
|
+
)
|
|
12
|
+
throw new TypeError('Variants need a lowercase data-* attribute and a value');
|
|
13
|
+
return `${Constructor.tag}[${variant.attribute}="${variant.value}"]`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyNode } from '../core/types.js';
|
|
2
|
+
|
|
3
|
+
/** Internal extension hook, after the entire native/reconstructed tree is available. */
|
|
4
|
+
export const initializeElement = Symbol('init');
|
|
5
|
+
type Initializable = AnyNode & { [initializeElement]?: () => void };
|
|
6
|
+
let pending: Initializable[] | undefined;
|
|
7
|
+
|
|
8
|
+
export function initializeNode(node: AnyNode): void {
|
|
9
|
+
if (pending) pending.push(node);
|
|
10
|
+
else (node as Initializable)[initializeElement]?.();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Delay hooks until clone/import has restored references and copied model features. */
|
|
14
|
+
export function withInitialization<Result>(callback: () => Result): Result {
|
|
15
|
+
if (pending) return callback();
|
|
16
|
+
pending = [];
|
|
17
|
+
try {
|
|
18
|
+
const result = callback();
|
|
19
|
+
// Initializers are idempotent; nested adoptions can append work while hooks run.
|
|
20
|
+
for (const node of pending) if (!node.isDestroyed) node[initializeElement]?.();
|
|
21
|
+
return result;
|
|
22
|
+
} finally {
|
|
23
|
+
pending = undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/services/registry.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { validateAttributeValue } from '../core/attribute-validation.js';
|
|
|
4
4
|
import { validateNativeElement } from '../core/native-validation.js';
|
|
5
5
|
import type { AnyNode } from '../core/types.js';
|
|
6
6
|
import type { ParentNode } from '../mixins/types.js';
|
|
7
|
+
import { withInternalStructure } from '../mixins/parent-internals.js';
|
|
8
|
+
import { elementKey } from './element-key.js';
|
|
9
|
+
import { initializeNode, withInitialization } from './initialization.js';
|
|
7
10
|
import type { DrawingContext } from './context.js';
|
|
8
11
|
import type { ElementClass } from './types.js';
|
|
9
12
|
|
|
@@ -11,6 +14,8 @@ export class Registry {
|
|
|
11
14
|
private static readonly identity = new WeakMap<SVGElement, AnyNode>();
|
|
12
15
|
private readonly wrappers = new Map<SVGElement, AnyNode>();
|
|
13
16
|
private readonly constructors = new Map<string, ElementClass>();
|
|
17
|
+
/** Tags that have variants, with the attributes that select them. */
|
|
18
|
+
private readonly variantAttributes = new Map<string, Set<string>>();
|
|
14
19
|
|
|
15
20
|
constructor(private readonly context: DrawingContext) {}
|
|
16
21
|
|
|
@@ -19,18 +24,45 @@ export class Registry {
|
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
register<Options, Result extends AnyNode>(Constructor: ElementClass<Options, Result>): this {
|
|
22
|
-
const
|
|
27
|
+
const key = elementKey(Constructor);
|
|
28
|
+
const previous = this.constructors.get(key);
|
|
23
29
|
if (previous === Constructor) return this;
|
|
24
|
-
if (previous
|
|
25
|
-
throw new Error(
|
|
26
|
-
this.constructors.set(
|
|
30
|
+
if (previous)
|
|
31
|
+
throw new Error(`${Constructor.variant ? 'Variant' : 'Tag'} already registered: ${key}`);
|
|
32
|
+
this.constructors.set(key, Constructor);
|
|
33
|
+
const variant = Constructor.variant;
|
|
34
|
+
if (variant) {
|
|
35
|
+
const attributes = this.variantAttributes.get(Constructor.tag) ?? new Set<string>();
|
|
36
|
+
this.variantAttributes.set(Constructor.tag, attributes.add(variant.attribute));
|
|
37
|
+
}
|
|
27
38
|
return this;
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
/** The tag's base constructor, ignoring variants. */
|
|
30
42
|
get(tag: string): ElementClass | undefined {
|
|
31
43
|
return this.constructors.get(tag);
|
|
32
44
|
}
|
|
33
45
|
|
|
46
|
+
/** The constructor registered for the same tag or variant as `Constructor`, if any. */
|
|
47
|
+
registered(Constructor: ElementClass): ElementClass | undefined {
|
|
48
|
+
return this.constructors.get(elementKey(Constructor));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
variant(tag: string, attribute: string, value: string): ElementClass | undefined {
|
|
52
|
+
return this.constructors.get(`${tag}[${attribute}="${value}"]`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The constructor for a native element: a matching variant, else the tag's base. */
|
|
56
|
+
resolve(element: SVGElement): ElementClass | undefined {
|
|
57
|
+
const tag = this.context.dom.tag(element);
|
|
58
|
+
for (const attribute of this.variantAttributes.get(tag) ?? []) {
|
|
59
|
+
const value = element.getAttribute(attribute);
|
|
60
|
+
const Constructor = value === null ? undefined : this.variant(tag, attribute, value);
|
|
61
|
+
if (Constructor) return Constructor;
|
|
62
|
+
}
|
|
63
|
+
return this.constructors.get(tag);
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
/** Creates a fresh wrapper for binding initialization, without constructor defaults or mounting. */
|
|
35
67
|
createEmpty(tag: string, context = this.context): AnyNode {
|
|
36
68
|
const Constructor = this.get(tag);
|
|
@@ -61,6 +93,7 @@ export class Registry {
|
|
|
61
93
|
for (const node of this.wrappers.values()) this.forget(node);
|
|
62
94
|
this.wrappers.clear();
|
|
63
95
|
this.constructors.clear();
|
|
96
|
+
this.variantAttributes.clear();
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
adopt(element: SVGElement, parent?: ParentNode): AnyNode {
|
|
@@ -77,14 +110,17 @@ export class Registry {
|
|
|
77
110
|
ancestor;
|
|
78
111
|
ancestor = this.context.dom.svgParent(ancestor)
|
|
79
112
|
) {
|
|
80
|
-
const Constructor = this.
|
|
113
|
+
const Constructor = this.resolve(ancestor);
|
|
81
114
|
if (Constructor?.opaqueContent)
|
|
82
115
|
throw new Error('Cannot adopt descendants across an opaque native boundary');
|
|
83
116
|
for (const tag of Constructor?.forbiddenDescendantTags ?? []) forbidden.add(tag);
|
|
84
117
|
}
|
|
85
118
|
this.validateTree(element, forbidden);
|
|
86
|
-
|
|
87
|
-
return
|
|
119
|
+
// Adoption reproduces a native tree exactly, so it may fill owned children.
|
|
120
|
+
return withInitialization(() => {
|
|
121
|
+
const wrapped = withInternalStructure(() => this.wrapTree(element));
|
|
122
|
+
return parent ? parent.add(wrapped) : wrapped;
|
|
123
|
+
});
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
private assertAdoptionParent(parent: ParentNode | undefined): void {
|
|
@@ -103,7 +139,7 @@ export class Registry {
|
|
|
103
139
|
if (existing.context !== this.context) throw new Error('Cross-root adoption is unsupported');
|
|
104
140
|
return;
|
|
105
141
|
}
|
|
106
|
-
const Constructor = this.
|
|
142
|
+
const Constructor = this.resolve(element);
|
|
107
143
|
if (!this.context.dom.isSvg(element) || !Constructor)
|
|
108
144
|
throw new TypeError(`Unsupported SVG tag: ${tag}`);
|
|
109
145
|
Constructor[validateNativeElement]?.(element);
|
|
@@ -130,16 +166,17 @@ export class Registry {
|
|
|
130
166
|
private wrapTree(element: SVGElement): AnyNode {
|
|
131
167
|
const existing = Registry.wrapper(element);
|
|
132
168
|
if (existing) return existing;
|
|
133
|
-
const Constructor = this.
|
|
169
|
+
const Constructor = this.resolve(element)!;
|
|
134
170
|
const children = this.managedChildren(element);
|
|
135
171
|
const node = new Constructor(this.context, {}, element);
|
|
136
172
|
if (Constructor.isContainer)
|
|
137
173
|
(node as ParentNode).adoptChildren(children.map((child) => this.wrapTree(child)));
|
|
174
|
+
initializeNode(node);
|
|
138
175
|
return node;
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
private managedChildren(element: SVGElement): SVGElement[] {
|
|
142
|
-
const Constructor = this.
|
|
179
|
+
const Constructor = this.resolve(element);
|
|
143
180
|
if (Constructor?.opaqueContent) return [];
|
|
144
181
|
return this.context.dom
|
|
145
182
|
.children(element)
|
package/src/services/types.ts
CHANGED
|
@@ -7,6 +7,23 @@ import { validateNativeElement } from '../core/native-validation.js';
|
|
|
7
7
|
|
|
8
8
|
// Registry
|
|
9
9
|
|
|
10
|
+
/** Type-only capability for constructors whose options come from instance fields. */
|
|
11
|
+
export declare const creationOptions: unique symbol;
|
|
12
|
+
export type CreationOptions<Options, Instance> = Instance extends {
|
|
13
|
+
readonly [creationOptions]: infer Fields;
|
|
14
|
+
}
|
|
15
|
+
? Fields
|
|
16
|
+
: Options;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Selects a subclass for elements that share a tag, by the exact value of one `data-*`
|
|
20
|
+
* attribute. Instances always carry that value, and it can't change.
|
|
21
|
+
*/
|
|
22
|
+
export interface ElementVariant {
|
|
23
|
+
readonly attribute: string;
|
|
24
|
+
readonly value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
export interface ElementClass<Options = any, Result extends AnyNode = AnyNode> {
|
|
11
28
|
readonly tag: string;
|
|
12
29
|
readonly attributeNames: readonly string[];
|
|
@@ -15,6 +32,10 @@ export interface ElementClass<Options = any, Result extends AnyNode = AnyNode> {
|
|
|
15
32
|
readonly childTags?: readonly string[] | undefined;
|
|
16
33
|
readonly forbiddenDescendantTags?: readonly string[] | undefined;
|
|
17
34
|
readonly opaqueContent?: boolean;
|
|
35
|
+
/** Registers the class for one attribute value of its tag instead of the whole tag. */
|
|
36
|
+
readonly variant?: ElementVariant | undefined;
|
|
37
|
+
/** Children belong to the class: outside structural edits throw. */
|
|
38
|
+
readonly ownsChildren?: boolean;
|
|
18
39
|
[validateAttributeValue]?(name: string, value: AttributeValue): void;
|
|
19
40
|
[validateNativeElement]?(element: SVGElement): void;
|
|
20
41
|
new (context: DrawingContext, options: Options, existing?: any): Result;
|