@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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EditRules } from '../types/common.js';
|
|
2
|
+
import type { Component } from './component.js';
|
|
3
|
+
import type { DrawingContext } from '../services/context.js';
|
|
4
|
+
import type { ElementClass } from '../services/types.js';
|
|
5
|
+
|
|
6
|
+
export type PropValue = string | number | boolean | null;
|
|
7
|
+
export type ComponentProps = Readonly<Record<string, PropValue>>;
|
|
8
|
+
|
|
9
|
+
/** Standard decorators cannot distinguish props from other fields in the TS type system. */
|
|
10
|
+
export type ComponentFields<Instance extends Component> = {
|
|
11
|
+
[
|
|
12
|
+
Key in Exclude<keyof Instance, keyof Component> as Instance[Key] extends PropValue ? Key : never
|
|
13
|
+
]?: Instance[Key];
|
|
14
|
+
} & {
|
|
15
|
+
/** The saved size of a `resize: 'props'` component; other components reject it. */
|
|
16
|
+
readonly width?: number;
|
|
17
|
+
readonly height?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface PropOptions {
|
|
21
|
+
readonly label?: string;
|
|
22
|
+
readonly unit?: string;
|
|
23
|
+
readonly min?: number;
|
|
24
|
+
readonly max?: number;
|
|
25
|
+
readonly step?: number;
|
|
26
|
+
readonly options?: readonly PropValue[];
|
|
27
|
+
readonly type?: 'color';
|
|
28
|
+
readonly notation?: 'si';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ComponentOptions extends EditRules {
|
|
32
|
+
readonly type: string;
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly category?: string;
|
|
35
|
+
readonly version?: number;
|
|
36
|
+
readonly migrate?: (props: ComponentProps, version: number) => ComponentProps;
|
|
37
|
+
/** Default size for `resize: 'props'`, in user units. Defaults to 100. */
|
|
38
|
+
readonly width?: number;
|
|
39
|
+
readonly height?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ComponentDefinition extends ComponentOptions {
|
|
43
|
+
readonly category: string;
|
|
44
|
+
readonly version: number;
|
|
45
|
+
readonly rotate: NonNullable<EditRules['rotate']>;
|
|
46
|
+
readonly flip: boolean;
|
|
47
|
+
readonly resize: NonNullable<EditRules['resize']>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ComponentClass<Instance extends Component = Component> extends ElementClass<
|
|
51
|
+
ComponentProps,
|
|
52
|
+
Instance
|
|
53
|
+
> {
|
|
54
|
+
new (context: DrawingContext, props?: ComponentProps, existing?: SVGGElement): Instance;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An `@action` method: its name and undo label. */
|
|
58
|
+
export interface ActionDefinition {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly label: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PropDefinition extends PropOptions {
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly initial: PropValue;
|
|
66
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { AnyNode } from '../core/types.js';
|
|
2
|
+
import { Group } from '../elements/group.js';
|
|
3
|
+
import { Renderer, type TemplateInstance } from '../template/render.js';
|
|
4
|
+
import {
|
|
5
|
+
BindingPart,
|
|
6
|
+
TemplateCompiler,
|
|
7
|
+
type ContentItem,
|
|
8
|
+
type TemplateDescription,
|
|
9
|
+
} from '../template/parts.js';
|
|
10
|
+
import type { SvgTemplate } from '../template/svg-tag.js';
|
|
11
|
+
import { Guards } from '../mixins/guards.js';
|
|
12
|
+
import type { Component } from './component.js';
|
|
13
|
+
import { destroySubtrees } from '../core/destruction.js';
|
|
14
|
+
import { Attributes } from '../dom/attributes.js';
|
|
15
|
+
|
|
16
|
+
/** Reuses saved wrappers when their structure matches the class's current template. */
|
|
17
|
+
export class ComponentView {
|
|
18
|
+
#instance?: TemplateInstance;
|
|
19
|
+
#host?: AnyNode;
|
|
20
|
+
#strings?: TemplateStringsArray;
|
|
21
|
+
#parts: BindingPart[] = [];
|
|
22
|
+
readonly #owner: Component;
|
|
23
|
+
constructor(owner: Component) {
|
|
24
|
+
this.#owner = owner;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render(template: SvgTemplate): void {
|
|
28
|
+
if (
|
|
29
|
+
this.#instance &&
|
|
30
|
+
!this.#instance.isDestroyed &&
|
|
31
|
+
this.#instance.host.parent === this.#owner &&
|
|
32
|
+
this.#instance.template.strings === template.strings
|
|
33
|
+
) {
|
|
34
|
+
this.#instance.update(template);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
this.#host &&
|
|
39
|
+
!this.#host.isDestroyed &&
|
|
40
|
+
this.#host.parent === this.#owner &&
|
|
41
|
+
this.#strings === template.strings
|
|
42
|
+
) {
|
|
43
|
+
this.#update(template);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.#parts = [];
|
|
47
|
+
// The first template-power phase owns structural holes; rebuild saved dynamic trees
|
|
48
|
+
// through its renderer instead of trying to infer their binding layout here.
|
|
49
|
+
const content = TemplateCompiler.content(this.#owner.context.dom, template.strings);
|
|
50
|
+
TemplateCompiler.validate(content, this.#owner.context.registry);
|
|
51
|
+
const descriptions = this.#hasChildHoles(content)
|
|
52
|
+
? undefined
|
|
53
|
+
: content.filter((item): item is TemplateDescription => 'tag' in item);
|
|
54
|
+
const host = this.#owner.children.length === 1 ? this.#owner.children[0] : undefined;
|
|
55
|
+
if (
|
|
56
|
+
descriptions &&
|
|
57
|
+
host instanceof Group &&
|
|
58
|
+
this.#attributesMatch(host, {}) &&
|
|
59
|
+
this.#matches(host.children, descriptions)
|
|
60
|
+
) {
|
|
61
|
+
this.#bind(host.children, descriptions);
|
|
62
|
+
this.#host = host;
|
|
63
|
+
this.#strings = template.strings;
|
|
64
|
+
this.#instance = undefined;
|
|
65
|
+
this.#update(template);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const previous = [...this.#owner.children];
|
|
69
|
+
this.#instance = new Renderer(this.#owner).render(template);
|
|
70
|
+
if (previous.length) this.#owner[destroySubtrees](previous);
|
|
71
|
+
this.#host = undefined;
|
|
72
|
+
this.#strings = undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#attributesMatch(node: AnyNode, attributes: Readonly<Record<string, unknown>>): boolean {
|
|
76
|
+
const names = node.attributes.entries().map(([name]) => name);
|
|
77
|
+
names.push(...node.el.getAttributeNames().map(Attributes.normalizeName));
|
|
78
|
+
return names.every((name) => Object.hasOwn(attributes, name));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#hasChildHoles(content: readonly ContentItem[]): boolean {
|
|
82
|
+
return content.some((item) => {
|
|
83
|
+
if (!('tag' in item)) return item.parts.some((part) => typeof part === 'number');
|
|
84
|
+
const Constructor = this.#owner.context.registry.get(item.tag);
|
|
85
|
+
return this.#hasChildHoles(Constructor?.isText ? item.children : item.content);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#matches(nodes: readonly AnyNode[], descriptions: readonly TemplateDescription[]): boolean {
|
|
90
|
+
return (
|
|
91
|
+
nodes.length === descriptions.length &&
|
|
92
|
+
nodes.every((node, index) => {
|
|
93
|
+
const description = descriptions[index];
|
|
94
|
+
return (
|
|
95
|
+
!(Guards.isTextual(node) && description.text.length && description.children.length) &&
|
|
96
|
+
node.el.localName === description.tag &&
|
|
97
|
+
this.#attributesMatch(node, description.attributes) &&
|
|
98
|
+
(Guards.isParent(node)
|
|
99
|
+
? this.#matches(node.children, description.children)
|
|
100
|
+
: description.children.length === 0)
|
|
101
|
+
);
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#bind(nodes: readonly AnyNode[], descriptions: readonly TemplateDescription[]): void {
|
|
107
|
+
nodes.forEach((node, index) => {
|
|
108
|
+
const description = descriptions[index];
|
|
109
|
+
for (const [name, parts] of Object.entries(description.attributes))
|
|
110
|
+
this.#parts.push(new BindingPart(node, name, parts));
|
|
111
|
+
if (description.text.length && Guards.isTextual(node)) {
|
|
112
|
+
this.#parts.push(new BindingPart(node, null, description.text));
|
|
113
|
+
}
|
|
114
|
+
if (Guards.isParent(node)) this.#bind(node.children, description.children);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#update(template: SvgTemplate): void {
|
|
119
|
+
const changes = this.#parts.map((part) => ({ part, value: part.evaluate(template.values) }));
|
|
120
|
+
for (const { part, value } of changes) part.validate(value);
|
|
121
|
+
for (const { part, value } of changes) part.commit(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Opt-in class component rendering, saved props and temporary live state. */
|
|
2
|
+
export { Component } from './components/component.js';
|
|
3
|
+
export { component, prop, state, action } from './components/decorators.js';
|
|
4
|
+
export { ComponentTypes, isComponentChild } from './components/registry.js';
|
|
5
|
+
export type {
|
|
6
|
+
ActionDefinition,
|
|
7
|
+
ComponentClass,
|
|
8
|
+
ComponentDefinition,
|
|
9
|
+
ComponentFields,
|
|
10
|
+
ComponentOptions,
|
|
11
|
+
ComponentProps,
|
|
12
|
+
PropValue,
|
|
13
|
+
PropOptions,
|
|
14
|
+
PropDefinition,
|
|
15
|
+
} from './components/types.js';
|
package/src/core/node.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { Attributes } from '../dom/attributes.js';
|
|
|
4
4
|
import { Emitter } from '../events/emitter.js';
|
|
5
5
|
import { Matrix } from '../math/matrix.js';
|
|
6
6
|
import type { DrawingContext } from '../services/context.js';
|
|
7
|
-
import type { ElementClass } from '../services/types.js';
|
|
7
|
+
import type { ElementClass, ElementVariant } from '../services/types.js';
|
|
8
|
+
import { assertChildrenEditable, withOwnedChildren } from '../mixins/parent-internals.js';
|
|
8
9
|
import type { ParentNode } from '../mixins/types.js';
|
|
9
10
|
import type { AttributeValue, Bounds, CommonAttributes } from '../types/common.js';
|
|
10
11
|
import type { AnyNode, NodeEvents } from './types.js';
|
|
@@ -28,6 +29,10 @@ export class Node<
|
|
|
28
29
|
static readonly isText: boolean = false;
|
|
29
30
|
static readonly childTags: readonly string[] | undefined = undefined;
|
|
30
31
|
declare static readonly forbiddenDescendantTags: readonly string[] | undefined;
|
|
32
|
+
/** See `ElementVariant`: one `data-*` attribute value selects this class for its tag. */
|
|
33
|
+
static readonly variant: ElementVariant | undefined = undefined;
|
|
34
|
+
/** When true, only the class itself changes its children, through `withOwnChildren()`. */
|
|
35
|
+
static readonly ownsChildren: boolean = false;
|
|
31
36
|
|
|
32
37
|
readonly el: Element;
|
|
33
38
|
readonly attributes: Attributes;
|
|
@@ -61,6 +66,8 @@ export class Node<
|
|
|
61
66
|
this.allowedAttributes = schema;
|
|
62
67
|
for (const [name, value] of this.attributes.entries()) this.validateAttribute(name, value);
|
|
63
68
|
if (!existing) {
|
|
69
|
+
const variant = Constructor.variant;
|
|
70
|
+
if (variant) this.setAttributeValue(variant.attribute, variant.value);
|
|
64
71
|
this.attrs(options);
|
|
65
72
|
for (const { attribute, initial } of fields)
|
|
66
73
|
if (initial !== undefined && this.attributes.getNormalized(attribute) === undefined)
|
|
@@ -111,6 +118,19 @@ export class Node<
|
|
|
111
118
|
name = Attributes.normalizeName(name);
|
|
112
119
|
if (!this.allowedAttributes.has(name) && !/^(?:data|aria)-[\w-]+$/.test(name))
|
|
113
120
|
throw new TypeError(`Unsupported attribute: ${name}`);
|
|
121
|
+
if (name.startsWith('data-')) {
|
|
122
|
+
// A variant's identifying attribute is fixed, and other classes can't claim its value.
|
|
123
|
+
const Constructor = this.constructor as ElementClass;
|
|
124
|
+
const variant = Constructor.variant;
|
|
125
|
+
if (
|
|
126
|
+
variant?.attribute === name
|
|
127
|
+
? value !== variant.value
|
|
128
|
+
: value != null &&
|
|
129
|
+
(this.context.registry.variant(Constructor.tag, name, String(value)) ?? Constructor) !==
|
|
130
|
+
Constructor
|
|
131
|
+
)
|
|
132
|
+
throw new TypeError(`${name} selects the element class`);
|
|
133
|
+
}
|
|
114
134
|
if (value != null) Attributes.normalize(name, value);
|
|
115
135
|
if (name === 'transform' && value != null) Matrix.parse(String(value));
|
|
116
136
|
if (
|
|
@@ -123,6 +143,12 @@ export class Node<
|
|
|
123
143
|
(this.constructor as ElementClass)[validateAttributeValue]?.(name, value);
|
|
124
144
|
}
|
|
125
145
|
|
|
146
|
+
/** For classes with `static ownsChildren`: change this node's own children. */
|
|
147
|
+
protected withOwnChildren<Result>(callback: () => Result): Result {
|
|
148
|
+
this.assertLive();
|
|
149
|
+
return withOwnedChildren(this, callback);
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
getAttributeValue(name: string): string | number | undefined {
|
|
127
153
|
this.assertLive();
|
|
128
154
|
return this.attributes.get(name);
|
|
@@ -305,6 +331,7 @@ export class Node<
|
|
|
305
331
|
remove(): this {
|
|
306
332
|
return mutation(this, () => {
|
|
307
333
|
this.assertLive();
|
|
334
|
+
assertChildrenEditable(this.parent);
|
|
308
335
|
for (const node of this.descendants()) node.releaseCapturedPointers();
|
|
309
336
|
this.detachNativeElement();
|
|
310
337
|
this.context.bounds.invalidate();
|
|
@@ -316,6 +343,7 @@ export class Node<
|
|
|
316
343
|
destroy(): void {
|
|
317
344
|
if (this.isDestroyed) return;
|
|
318
345
|
this.assertLive();
|
|
346
|
+
assertChildrenEditable(this.parent);
|
|
319
347
|
this.context.defs.assertCanDestroy(
|
|
320
348
|
new Set(this === this.context.root ? this.context.registry.nodes : this.descendants()),
|
|
321
349
|
);
|
|
@@ -339,6 +367,9 @@ export class Node<
|
|
|
339
367
|
disposalOrder.push(node);
|
|
340
368
|
}
|
|
341
369
|
}
|
|
370
|
+
// Preflight ownership before any disposal side effect; owners destroyed together pass.
|
|
371
|
+
for (const root of roots)
|
|
372
|
+
if (root.parent && !ownedNodes.has(root.parent)) assertChildrenEditable(root.parent);
|
|
342
373
|
this.context.defs.assertCanDestroy(ownedNodes);
|
|
343
374
|
this.context.changes.beforeDisposal([...ownedNodes]);
|
|
344
375
|
for (const root of roots) this.context.changes.touchTree(root);
|
package/src/dom/images.ts
CHANGED
|
@@ -20,7 +20,8 @@ export class ImageLoader {
|
|
|
20
20
|
return () => {
|
|
21
21
|
probe.removeEventListener('load', loaded);
|
|
22
22
|
probe.removeEventListener('error', failed);
|
|
23
|
-
|
|
23
|
+
// Resetting an already completed source starts a second native error request.
|
|
24
|
+
if (!probe.complete) probe.removeAttribute('src');
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
}
|
package/src/elements/image.ts
CHANGED
|
@@ -44,7 +44,10 @@ export class SvgImage extends Shape<SVGImageElement, ImageOptions> {
|
|
|
44
44
|
|
|
45
45
|
constructor(context: DrawingContext, options: ImageOptions = {}, existing?: SVGImageElement) {
|
|
46
46
|
super(context, options, existing);
|
|
47
|
-
this.onDispose(() =>
|
|
47
|
+
this.onDispose(() => {
|
|
48
|
+
this.cancelRequest();
|
|
49
|
+
this.el.removeAttribute('href');
|
|
50
|
+
});
|
|
48
51
|
if (this.source) this.context.scheduler.enqueue(this, '#image');
|
|
49
52
|
}
|
|
50
53
|
|
package/src/interchange/clone.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { dependencyRoots, remapIds } from './references.js';
|
|
|
7
7
|
import { classifyReference } from '../resources/references.js';
|
|
8
8
|
import { preflight, reconstruct, validateCloneContent } from './reconstruct.js';
|
|
9
9
|
import type { CloneOptions } from './types.js';
|
|
10
|
-
import { copyTextPolicies } from '../text/
|
|
10
|
+
import { copyTextPolicies } from '../text/policies.js';
|
|
11
11
|
|
|
12
12
|
export function cloneNode<Node extends AnyNode>(source: Node, options: CloneOptions = {}): Node {
|
|
13
13
|
return source.context.changes.run(source, () => {
|
|
@@ -24,8 +24,9 @@ export function cloneNode<Node extends AnyNode>(source: Node, options: CloneOpti
|
|
|
24
24
|
const originals = [source, ...dependencies];
|
|
25
25
|
const copies = new Map<AnyNode, SVGElement>();
|
|
26
26
|
const roots = originals.map((node) => snapshot(node, copies));
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// Copies are rebuilt by adoption, so each must resolve back to its original's class.
|
|
28
|
+
for (const [original, element] of copies)
|
|
29
|
+
if (source.context.registry.resolve(element) !== original.constructor)
|
|
29
30
|
throw new TypeError('Clone requires the exact registered constructor for each managed tag');
|
|
30
31
|
for (const original of copies.keys())
|
|
31
32
|
if (Guards.isTransformable(original) && !original.geometryMatrix().inverse())
|
package/src/interchange/dom.ts
CHANGED
|
@@ -2,19 +2,29 @@ import { SVG_NAMESPACE, XLINK_NAMESPACE } from '../catalog/namespaces.js';
|
|
|
2
2
|
|
|
3
3
|
/** Parsing and serialization operate on detached nodes in the destination document. */
|
|
4
4
|
export class InterchangeDom {
|
|
5
|
-
static parse(owner: SVGElement, source: string): SVGElement[] {
|
|
5
|
+
static parse(owner: SVGElement, source: string, document = false): SVGElement[] {
|
|
6
6
|
if (typeof source !== 'string' || source.length > 1_000_000)
|
|
7
7
|
throw new TypeError('SVG import requires at most 1,000,000 characters');
|
|
8
|
-
|
|
8
|
+
// A document prolog may hold an XML declaration and a DOCTYPE without an internal subset.
|
|
9
|
+
const prolog = document
|
|
10
|
+
? /^\s*(?:<\?xml\s[^?]*\?>)?(?:\s|<!--(?:[^-]|-(?!->))*-->)*(?:<!DOCTYPE\s+svg\b[^[>]*>)?/i.exec(
|
|
11
|
+
source,
|
|
12
|
+
)![0]
|
|
13
|
+
: '';
|
|
14
|
+
const body = source.slice(prolog.length);
|
|
15
|
+
if (/<!\s*(?:DOCTYPE|ENTITY)|<\?/i.test(body))
|
|
9
16
|
throw new TypeError('SVG import rejects declarations, entities and processing instructions');
|
|
10
17
|
const Parser = owner.ownerDocument.defaultView?.DOMParser ?? globalThis.DOMParser;
|
|
11
18
|
if (!Parser) throw new Error('SVG import requires DOMParser');
|
|
12
19
|
const parsed = new Parser().parseFromString(
|
|
13
|
-
`<svg xmlns="${SVG_NAMESPACE}" xmlns:xlink="${XLINK_NAMESPACE}">${
|
|
20
|
+
`<svg xmlns="${SVG_NAMESPACE}" xmlns:xlink="${XLINK_NAMESPACE}">${body}</svg>`,
|
|
14
21
|
'image/svg+xml',
|
|
15
22
|
);
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const failure = parsed.getElementsByTagName('parsererror')[0];
|
|
24
|
+
if (failure)
|
|
25
|
+
throw new SyntaxError(
|
|
26
|
+
`Not valid SVG: ${failure.textContent?.trim().split(/\r?\n/)[0] || 'Invalid SVG markup'}`,
|
|
27
|
+
);
|
|
18
28
|
const roots: SVGElement[] = [];
|
|
19
29
|
for (const node of parsed.documentElement.childNodes) {
|
|
20
30
|
if (node.nodeType === 8 || (node.nodeType === 3 && !node.textContent.trim())) continue;
|
|
@@ -27,6 +37,25 @@ export class InterchangeDom {
|
|
|
27
37
|
element.replaceChild(owner.ownerDocument.createTextNode(child.textContent), child);
|
|
28
38
|
return roots;
|
|
29
39
|
}
|
|
40
|
+
/** Called after passive validation so even discarded envelope attributes are checked. */
|
|
41
|
+
static unwrap(roots: SVGElement[], presentation: boolean): SVGElement[] {
|
|
42
|
+
const root = roots[0];
|
|
43
|
+
if (roots.length !== 1 || root.localName !== 'svg') return roots;
|
|
44
|
+
const children = [...root.children] as SVGElement[];
|
|
45
|
+
if (!presentation) return children;
|
|
46
|
+
const group = root.ownerDocument.createElementNS(SVG_NAMESPACE, 'g');
|
|
47
|
+
for (const attribute of [...root.attributes])
|
|
48
|
+
if (
|
|
49
|
+
!['x', 'y', 'width', 'height', 'viewBox', 'preserveAspectRatio', 'version'].includes(
|
|
50
|
+
attribute.name,
|
|
51
|
+
) &&
|
|
52
|
+
attribute.namespaceURI !== 'http://www.w3.org/2000/xmlns/'
|
|
53
|
+
)
|
|
54
|
+
group.setAttributeNS(attribute.namespaceURI, attribute.name, attribute.value);
|
|
55
|
+
if (!group.attributes.length) return children;
|
|
56
|
+
group.append(...root.childNodes);
|
|
57
|
+
return [group];
|
|
58
|
+
}
|
|
30
59
|
static serialize(element: SVGElement): string {
|
|
31
60
|
const Serializer = element.ownerDocument.defaultView?.XMLSerializer ?? globalThis.XMLSerializer;
|
|
32
61
|
if (!Serializer) throw new Error('SVG export requires XMLSerializer');
|
|
@@ -8,6 +8,31 @@ import { InterchangeDom } from './dom.js';
|
|
|
8
8
|
import { preflight, reconstruct } from './reconstruct.js';
|
|
9
9
|
import { validateImportContent } from './policy.js';
|
|
10
10
|
import type { ExportOptions, ImportOptions } from './types.js';
|
|
11
|
+
import { BBox } from '../math/bbox.js';
|
|
12
|
+
import { isNestedViewport, viewportLength } from '../services/viewport.js';
|
|
13
|
+
import { Attributes } from '../dom/attributes.js';
|
|
14
|
+
|
|
15
|
+
function frameContent(source: AnyNode, envelope: SVGElement, padding: number): void {
|
|
16
|
+
const bounds = source.bounds('root');
|
|
17
|
+
if (bounds.status === 'unavailable')
|
|
18
|
+
throw new Error(`Cannot frame SVG content: ${bounds.reason}`);
|
|
19
|
+
const box =
|
|
20
|
+
'box' in bounds
|
|
21
|
+
? bounds.box
|
|
22
|
+
: {
|
|
23
|
+
x: 0,
|
|
24
|
+
y: 0,
|
|
25
|
+
width: BBox.numeric(source.context.root?.getAttributeValue('width'), 800) ?? 800,
|
|
26
|
+
height: BBox.numeric(source.context.root?.getAttributeValue('height'), 600) ?? 600,
|
|
27
|
+
};
|
|
28
|
+
const width = box.width + padding * 2,
|
|
29
|
+
height = box.height + padding * 2;
|
|
30
|
+
if (![box.x - padding, box.y - padding, width, height].every(Number.isFinite))
|
|
31
|
+
throw new RangeError('SVG content frame must be finite');
|
|
32
|
+
envelope.setAttribute('viewBox', `${box.x - padding} ${box.y - padding} ${width} ${height}`);
|
|
33
|
+
envelope.setAttribute('width', String(width));
|
|
34
|
+
envelope.setAttribute('height', String(height));
|
|
35
|
+
}
|
|
11
36
|
|
|
12
37
|
export function exportSvg(source: AnyNode, options: ExportOptions = {}): string {
|
|
13
38
|
source.assertLive();
|
|
@@ -15,6 +40,13 @@ export function exportSvg(source: AnyNode, options: ExportOptions = {}): string
|
|
|
15
40
|
throw new TypeError('Unknown SVG export format');
|
|
16
41
|
if (options.resources !== undefined && !['referenced', 'none'].includes(options.resources))
|
|
17
42
|
throw new TypeError('Unknown export resource policy');
|
|
43
|
+
if (options.frame !== undefined && options.frame !== 'content')
|
|
44
|
+
throw new TypeError('Unknown SVG frame');
|
|
45
|
+
if (options.frame && options.format === 'subtree')
|
|
46
|
+
throw new TypeError('Content framing requires a document export');
|
|
47
|
+
const padding = options.padding ?? 0;
|
|
48
|
+
if (!Number.isFinite(padding) || padding < 0)
|
|
49
|
+
throw new RangeError('SVG padding must be nonnegative and finite');
|
|
18
50
|
source.flush();
|
|
19
51
|
const dependencies = options.resources === 'none' ? [] : dependencyRoots(source, 'local');
|
|
20
52
|
const copies = new Map<AnyNode, SVGElement>();
|
|
@@ -25,20 +57,45 @@ export function exportSvg(source: AnyNode, options: ExportOptions = {}): string
|
|
|
25
57
|
return InterchangeDom.serialize(element);
|
|
26
58
|
}
|
|
27
59
|
const document = element.ownerDocument;
|
|
60
|
+
const framedSubtree = options.frame === 'content' && source !== source.context.root;
|
|
28
61
|
const envelope =
|
|
29
|
-
element.localName === 'svg'
|
|
62
|
+
element.localName === 'svg' && !framedSubtree
|
|
63
|
+
? element
|
|
64
|
+
: document.createElementNS(SVG_NAMESPACE, 'svg');
|
|
30
65
|
if (envelope !== element) {
|
|
31
66
|
for (const name of ['width', 'height', 'viewBox', 'preserveAspectRatio']) {
|
|
32
67
|
const value = source.context.root?.getAttributeValue(name);
|
|
33
68
|
if (value !== undefined) envelope.setAttribute(name, String(value));
|
|
34
69
|
}
|
|
35
|
-
|
|
70
|
+
if (framedSubtree && source.parent) {
|
|
71
|
+
// The snapshot retains its own transform and any nested SVG viewport. Only
|
|
72
|
+
// the omitted ancestors need mapping into the document-space content frame.
|
|
73
|
+
const mapping = document.createElementNS(SVG_NAMESPACE, 'g');
|
|
74
|
+
mapping.setAttribute('transform', source.context.bounds.matrix(source.parent).toString());
|
|
75
|
+
mapping.append(element);
|
|
76
|
+
envelope.append(mapping);
|
|
77
|
+
} else envelope.append(element);
|
|
36
78
|
}
|
|
37
79
|
if (dependencies.length) {
|
|
38
80
|
const defs = document.createElementNS(SVG_NAMESPACE, 'defs');
|
|
39
81
|
for (const dependency of dependencies) defs.append(snapshot(dependency, copies));
|
|
40
82
|
envelope.prepend(defs);
|
|
41
83
|
}
|
|
84
|
+
if (options.frame === 'content') {
|
|
85
|
+
frameContent(source, envelope, padding);
|
|
86
|
+
// Reframing changes percentage bases. Preserve each copied nested viewport's
|
|
87
|
+
// original placement and dimensions, including omitted 100% sizes.
|
|
88
|
+
for (const [node, copy] of copies) {
|
|
89
|
+
if (!isNestedViewport(node)) continue;
|
|
90
|
+
for (const name of ['x', 'y', 'width', 'height'] as const) {
|
|
91
|
+
const axis = name === 'x' || name === 'width' ? 'width' : 'height';
|
|
92
|
+
copy.setAttribute(
|
|
93
|
+
name,
|
|
94
|
+
String(viewportLength(node, name, axis, name === axis ? '100%' : 0)),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
42
99
|
return InterchangeDom.serialize(envelope);
|
|
43
100
|
}
|
|
44
101
|
|
|
@@ -51,8 +108,22 @@ export function importSvg(
|
|
|
51
108
|
assertCanAddChildren(parent);
|
|
52
109
|
const index = options.index ?? parent.children.length;
|
|
53
110
|
validateIndex(index, parent.children.length);
|
|
54
|
-
|
|
111
|
+
if (
|
|
112
|
+
options.document !== undefined &&
|
|
113
|
+
typeof options.document !== 'boolean' &&
|
|
114
|
+
!['presentation', 'replace'].includes(options.document)
|
|
115
|
+
)
|
|
116
|
+
throw new TypeError('Unknown document import mode');
|
|
117
|
+
if (
|
|
118
|
+
options.document === 'replace' &&
|
|
119
|
+
(parent !== parent.context.root || parent.children.length)
|
|
120
|
+
)
|
|
121
|
+
throw new TypeError('Document replacement requires an empty owning drawing');
|
|
122
|
+
let roots = InterchangeDom.parse(parent.el, markup, !!options.document);
|
|
55
123
|
validateImportContent(roots, options);
|
|
124
|
+
const replace =
|
|
125
|
+
options.document === 'replace' && roots.length === 1 && roots[0].localName === 'svg';
|
|
126
|
+
if (options.document) roots = InterchangeDom.unwrap(roots, options.document !== true);
|
|
56
127
|
preflight(parent.context, roots);
|
|
57
128
|
remapIds(parent, roots);
|
|
58
129
|
return reconstruct(parent, roots, (nodes) => {
|
|
@@ -60,6 +131,9 @@ export function importSvg(
|
|
|
60
131
|
for (const node of nodes) parent[validateChild](node);
|
|
61
132
|
for (let position = 0; position < nodes.length; position++)
|
|
62
133
|
parent.add(nodes[position], index + position);
|
|
134
|
+
if (replace)
|
|
135
|
+
for (const name of Attributes.common)
|
|
136
|
+
if (!['id', 'class'].includes(name)) parent.setAttributeValue(name, undefined);
|
|
63
137
|
return Object.freeze(nodes);
|
|
64
138
|
});
|
|
65
139
|
});
|
|
@@ -89,6 +89,12 @@ function validateUrl(tag: string, name: string, value: string, options: ImportOp
|
|
|
89
89
|
if (reference) return;
|
|
90
90
|
if (name === 'href') {
|
|
91
91
|
if (value === '') return;
|
|
92
|
+
// Image documents are passive resources; keep embedded image files restorable.
|
|
93
|
+
if (
|
|
94
|
+
tag === 'image' &&
|
|
95
|
+
/^data:image\/(?:png|jpe?g|gif|webp|avif|svg\+xml)(?:;[^,]*)?,/i.test(value)
|
|
96
|
+
)
|
|
97
|
+
return;
|
|
92
98
|
if (
|
|
93
99
|
options.externalUrls === 'http' &&
|
|
94
100
|
['image', 'a', 'use'].includes(tag) &&
|
|
@@ -8,6 +8,8 @@ import { validateNativeElement } from '../core/native-validation.js';
|
|
|
8
8
|
import { destroySubtrees } from '../core/destruction.js';
|
|
9
9
|
import { InterchangeDom } from './dom.js';
|
|
10
10
|
import type { AttributeBindings } from '../catalog/namespaces.js';
|
|
11
|
+
import { withInternalStructure } from '../mixins/parent-internals.js';
|
|
12
|
+
import { withInitialization } from '../services/initialization.js';
|
|
11
13
|
|
|
12
14
|
/** Pure validation for the complete forest, before Registry can claim a wrapper. */
|
|
13
15
|
export function preflight(
|
|
@@ -16,7 +18,7 @@ export function preflight(
|
|
|
16
18
|
schemas: ReadonlyMap<SVGElement, ReadonlySet<string>> = new Map(),
|
|
17
19
|
): void {
|
|
18
20
|
function visit(element: SVGElement, forbidden: ReadonlySet<string>): void {
|
|
19
|
-
const Constructor = context.registry.
|
|
21
|
+
const Constructor = context.registry.resolve(element) as ElementClass & {
|
|
20
22
|
attributeBindings?: AttributeBindings;
|
|
21
23
|
};
|
|
22
24
|
if (
|
|
@@ -90,22 +92,24 @@ export function reconstruct<Result>(
|
|
|
90
92
|
): Result {
|
|
91
93
|
const before = new Set(owner.context.registry.nodes);
|
|
92
94
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
return withInitialization(() => {
|
|
96
|
+
const nodes = roots.map((element) => owner.context.registry.adopt(element));
|
|
97
|
+
for (const node of owner.context.registry.nodes)
|
|
98
|
+
if (!before.has(node))
|
|
99
|
+
for (const name of ['tabindex', 'role']) {
|
|
100
|
+
const value = node.el.getAttribute(name);
|
|
101
|
+
if (value !== null) {
|
|
102
|
+
node.defineAttribute(name);
|
|
103
|
+
node.setAttributeValue(name, value);
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
return finish(nodes);
|
|
107
|
+
});
|
|
104
108
|
} catch (error) {
|
|
105
109
|
const created = owner.context.registry.nodes.filter((node) => !before.has(node));
|
|
106
110
|
if (created.length) {
|
|
107
111
|
try {
|
|
108
|
-
owner[destroySubtrees](created);
|
|
112
|
+
withInternalStructure(() => owner[destroySubtrees](created));
|
|
109
113
|
} catch (cleanupError) {
|
|
110
114
|
throw new AggregateError(
|
|
111
115
|
[error, cleanupError],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyNode } from '../core/types.js';
|
|
2
|
-
import { classifyReference, resolveReference } from '../resources/references.js';
|
|
2
|
+
import { classifyReference, isLink, resolveReference } from '../resources/references.js';
|
|
3
3
|
import { referenceValue } from '../services/reference-value.js';
|
|
4
4
|
import { InterchangeDom } from './dom.js';
|
|
5
5
|
import { XLINK_NAMESPACE } from '../catalog/namespaces.js';
|
|
@@ -10,10 +10,13 @@ export function dependencyRoots(source: AnyNode, references: 'typed' | 'local'):
|
|
|
10
10
|
const pending = [...included];
|
|
11
11
|
for (let index = 0; index < pending.length; index++) {
|
|
12
12
|
const node = pending[index];
|
|
13
|
-
|
|
13
|
+
// Links are not dependencies: copying a connector never copies the shapes it joins.
|
|
14
|
+
const targets = new Set(
|
|
15
|
+
[...node.references].filter(([name]) => !isLink(name)).map(([, target]) => target),
|
|
16
|
+
);
|
|
14
17
|
if (references === 'local')
|
|
15
18
|
for (const [name, value] of node.attributes.entries()) {
|
|
16
|
-
if (!classifyReference(name, value)) continue;
|
|
19
|
+
if (!classifyReference(name, value) || isLink(name)) continue;
|
|
17
20
|
// Hyperlinks navigate; they are not rendering dependencies.
|
|
18
21
|
if (name === 'href' && ['a', 'image'].includes(node.el.localName)) continue;
|
|
19
22
|
const target = resolveReference(node, name);
|
|
@@ -87,10 +90,15 @@ export function restoreLocalReferences(nodes: readonly AnyNode[]): void {
|
|
|
87
90
|
.map((node) => [String(node.getAttributeValue('id')), node]),
|
|
88
91
|
);
|
|
89
92
|
for (const node of all)
|
|
90
|
-
for (const [name, value] of node.attributes.entries()) {
|
|
93
|
+
for (const [name, value] of [...node.attributes.entries()]) {
|
|
91
94
|
const reference = classifyReference(name, value);
|
|
92
95
|
if (!reference) continue;
|
|
93
96
|
const target = ids.get(reference.id);
|
|
97
|
+
// A connector pasted without its target keeps that end loose.
|
|
98
|
+
if (!target && isLink(name)) {
|
|
99
|
+
node.setAttributeValue(name, undefined);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
94
102
|
if (!target)
|
|
95
103
|
throw new TypeError(`Local reference is outside the imported forest: ${reference.id}`);
|
|
96
104
|
if (name === 'href' && ['a', 'image'].includes(node.el.localName)) continue;
|
package/src/interchange/types.ts
CHANGED
|
@@ -10,9 +10,18 @@ export interface ExportOptions {
|
|
|
10
10
|
format?: 'document' | 'subtree';
|
|
11
11
|
/** Include reachable typed and unambiguous local references. CSS URLs are not traversed. */
|
|
12
12
|
resources?: 'referenced' | 'none';
|
|
13
|
+
/** Frame geometric content in document coordinates rather than the current camera. */
|
|
14
|
+
frame?: 'content';
|
|
15
|
+
/** Nonnegative drawing units around a content frame; defaults to zero. */
|
|
16
|
+
padding?: number;
|
|
13
17
|
}
|
|
14
18
|
export interface ImportOptions {
|
|
15
19
|
index?: number;
|
|
16
|
-
/**
|
|
20
|
+
/** true unwraps children; 'presentation' retains scoped root attributes without
|
|
21
|
+
* the viewport. 'replace' additionally clears configured root presentation for a
|
|
22
|
+
* full document, and requires an empty owning drawing. Fragments keep defaults. */
|
|
23
|
+
document?: boolean | 'presentation' | 'replace';
|
|
24
|
+
/** External URLs are rejected by default; embedded data images are allowed on image elements.
|
|
25
|
+
* Opt in to absolute HTTP(S) image/link/Use URLs. */
|
|
17
26
|
externalUrls?: 'reject' | 'http';
|
|
18
27
|
}
|