@rasamjs/core 0.2.0 → 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.
Files changed (162) hide show
  1. package/API.md +14 -4
  2. package/CHANGELOG.md +59 -0
  3. package/COMPONENTS.md +229 -0
  4. package/README.md +39 -6
  5. package/RELEASING.md +4 -2
  6. package/dist/advanced.d.ts +1 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/catalog/fragment.d.ts +2 -2
  9. package/dist/catalog/fragment.d.ts.map +1 -1
  10. package/dist/catalog/fragment.js.map +1 -1
  11. package/dist/changes/derived.d.ts +10 -0
  12. package/dist/changes/derived.d.ts.map +1 -0
  13. package/dist/changes/derived.js +14 -0
  14. package/dist/changes/derived.js.map +1 -0
  15. package/dist/changes/restore.d.ts.map +1 -1
  16. package/dist/changes/restore.js +22 -4
  17. package/dist/changes/restore.js.map +1 -1
  18. package/dist/changes/service.d.ts +5 -0
  19. package/dist/changes/service.d.ts.map +1 -1
  20. package/dist/changes/service.js +16 -2
  21. package/dist/changes/service.js.map +1 -1
  22. package/dist/components/changes.d.ts +34 -0
  23. package/dist/components/changes.d.ts.map +1 -0
  24. package/dist/components/changes.js +119 -0
  25. package/dist/components/changes.js.map +1 -0
  26. package/dist/components/component.d.ts +62 -0
  27. package/dist/components/component.d.ts.map +1 -0
  28. package/dist/components/component.js +355 -0
  29. package/dist/components/component.js.map +1 -0
  30. package/dist/components/decorators.d.ts +7 -0
  31. package/dist/components/decorators.d.ts.map +1 -0
  32. package/dist/components/decorators.js +81 -0
  33. package/dist/components/decorators.js.map +1 -0
  34. package/dist/components/metadata.d.ts +10 -0
  35. package/dist/components/metadata.d.ts.map +1 -0
  36. package/dist/components/metadata.js +85 -0
  37. package/dist/components/metadata.js.map +1 -0
  38. package/dist/components/registry.d.ts +24 -0
  39. package/dist/components/registry.d.ts.map +1 -0
  40. package/dist/components/registry.js +78 -0
  41. package/dist/components/registry.js.map +1 -0
  42. package/dist/components/types.d.ts +54 -0
  43. package/dist/components/types.d.ts.map +1 -0
  44. package/dist/components/types.js +2 -0
  45. package/dist/components/types.js.map +1 -0
  46. package/dist/components/view.d.ts +9 -0
  47. package/dist/components/view.d.ts.map +1 -0
  48. package/dist/components/view.js +104 -0
  49. package/dist/components/view.js.map +1 -0
  50. package/dist/components.d.ts +6 -0
  51. package/dist/components.d.ts.map +1 -0
  52. package/dist/components.js +5 -0
  53. package/dist/components.js.map +1 -0
  54. package/dist/core/node.d.ts +7 -0
  55. package/dist/core/node.d.ts.map +1 -1
  56. package/dist/core/node.js +30 -0
  57. package/dist/core/node.js.map +1 -1
  58. package/dist/interchange/clone.d.ts.map +1 -1
  59. package/dist/interchange/clone.js +3 -2
  60. package/dist/interchange/clone.js.map +1 -1
  61. package/dist/interchange/reconstruct.d.ts.map +1 -1
  62. package/dist/interchange/reconstruct.js +16 -12
  63. package/dist/interchange/reconstruct.js.map +1 -1
  64. package/dist/interchange/references.d.ts.map +1 -1
  65. package/dist/interchange/references.js +10 -4
  66. package/dist/interchange/references.js.map +1 -1
  67. package/dist/mixins/parent-internals.d.ts +6 -0
  68. package/dist/mixins/parent-internals.d.ts.map +1 -1
  69. package/dist/mixins/parent-internals.js +34 -0
  70. package/dist/mixins/parent-internals.js.map +1 -1
  71. package/dist/mixins/parent.d.ts.map +1 -1
  72. package/dist/mixins/parent.js +6 -1
  73. package/dist/mixins/parent.js.map +1 -1
  74. package/dist/mixins/types.d.ts +2 -2
  75. package/dist/mixins/types.d.ts.map +1 -1
  76. package/dist/resources/references.d.ts +2 -0
  77. package/dist/resources/references.d.ts.map +1 -1
  78. package/dist/resources/references.js +3 -3
  79. package/dist/resources/references.js.map +1 -1
  80. package/dist/services/catalog.d.ts.map +1 -1
  81. package/dist/services/catalog.js +5 -3
  82. package/dist/services/catalog.js.map +1 -1
  83. package/dist/services/defs-manager.d.ts +2 -0
  84. package/dist/services/defs-manager.d.ts.map +1 -1
  85. package/dist/services/defs-manager.js +4 -0
  86. package/dist/services/defs-manager.js.map +1 -1
  87. package/dist/services/element-key.d.ts +4 -0
  88. package/dist/services/element-key.d.ts.map +1 -0
  89. package/dist/services/element-key.js +12 -0
  90. package/dist/services/element-key.js.map +1 -0
  91. package/dist/services/initialization.d.ts +7 -0
  92. package/dist/services/initialization.d.ts.map +1 -0
  93. package/dist/services/initialization.js +27 -0
  94. package/dist/services/initialization.js.map +1 -0
  95. package/dist/services/registry.d.ts +8 -0
  96. package/dist/services/registry.d.ts.map +1 -1
  97. package/dist/services/registry.js +45 -10
  98. package/dist/services/registry.js.map +1 -1
  99. package/dist/services/types.d.ts +17 -0
  100. package/dist/services/types.d.ts.map +1 -1
  101. package/dist/template/parts.d.ts +12 -6
  102. package/dist/template/parts.d.ts.map +1 -1
  103. package/dist/template/parts.js +36 -29
  104. package/dist/template/parts.js.map +1 -1
  105. package/dist/template/render.d.ts +6 -7
  106. package/dist/template/render.d.ts.map +1 -1
  107. package/dist/template/render.js +188 -44
  108. package/dist/template/render.js.map +1 -1
  109. package/dist/template/svg-tag.d.ts +5 -1
  110. package/dist/template/svg-tag.d.ts.map +1 -1
  111. package/dist/template/svg-tag.js.map +1 -1
  112. package/dist/text/internals.d.ts +1 -0
  113. package/dist/text/internals.d.ts.map +1 -1
  114. package/dist/types/common.d.ts +27 -0
  115. package/dist/types/common.d.ts.map +1 -1
  116. package/package.json +156 -148
  117. package/reports/adoption.json +20 -20
  118. package/reports/animation-benchmark.json +612 -612
  119. package/reports/benchmark.json +40 -40
  120. package/reports/components-benchmark-before.json +10 -0
  121. package/reports/components-benchmark.json +10 -0
  122. package/reports/creation.json +26 -26
  123. package/reports/interchange-benchmark.json +85 -85
  124. package/reports/public-api-contract.json +334 -17
  125. package/reports/size-components.json +102 -0
  126. package/reports/size-parity.json +47 -34
  127. package/reports/size.json +6 -4
  128. package/reports/svgjs-comparison.json +3100 -3100
  129. package/reports/svgjs-comparison.md +16 -16
  130. package/reports/svgjs-parity-current.json +135 -42
  131. package/src/advanced.ts +1 -1
  132. package/src/catalog/fragment.ts +2 -2
  133. package/src/changes/derived.ts +19 -0
  134. package/src/changes/restore.ts +27 -4
  135. package/src/changes/service.ts +21 -6
  136. package/src/components/changes.ts +131 -0
  137. package/src/components/component.ts +401 -0
  138. package/src/components/decorators.ts +104 -0
  139. package/src/components/metadata.ts +111 -0
  140. package/src/components/registry.ts +79 -0
  141. package/src/components/types.ts +66 -0
  142. package/src/components/view.ts +123 -0
  143. package/src/components.ts +15 -0
  144. package/src/core/node.ts +32 -1
  145. package/src/interchange/clone.ts +3 -2
  146. package/src/interchange/reconstruct.ts +16 -12
  147. package/src/interchange/references.ts +12 -4
  148. package/src/mixins/parent-internals.ts +37 -0
  149. package/src/mixins/parent.ts +13 -4
  150. package/src/mixins/types.ts +2 -2
  151. package/src/resources/references.ts +5 -5
  152. package/src/services/catalog.ts +5 -3
  153. package/src/services/defs-manager.ts +5 -0
  154. package/src/services/element-key.ts +14 -0
  155. package/src/services/initialization.ts +25 -0
  156. package/src/services/registry.ts +47 -10
  157. package/src/services/types.ts +21 -0
  158. package/src/template/parts.ts +54 -39
  159. package/src/template/render.ts +211 -54
  160. package/src/template/svg-tag.ts +6 -1
  161. package/src/text/internals.ts +1 -0
  162. package/src/types/common.ts +29 -0
@@ -1,73 +1,180 @@
1
1
  import { mutation } from '../changes/mutation.js';
2
2
  import type { AnyNode } from '../core/types.js';
3
+ import { destroySubtrees } from '../core/destruction.js';
3
4
  import type { ParentNode } from '../mixins/types.js';
4
5
  import { Guards } from '../mixins/guards.js';
5
6
  import { Group } from '../elements/group.js';
6
7
  import type { Registry } from '../services/registry.js';
8
+ import type { ElementClass } from '../services/types.js';
7
9
  import { BindingPart, TemplateCompiler } from './parts.js';
8
- import type { TemplateDescription } from './parts.js';
9
- import type { SvgTemplate } from './svg-tag.js';
10
+ import type { ContentItem, TemplateDescription } from './parts.js';
11
+ import { SvgTemplate } from './svg-tag.js';
12
+ import type { TemplateValue } from './svg-tag.js';
10
13
  import { appendTextSlot, type TextBinding } from '../text/internals.js';
11
14
 
12
- export class TemplateInstance {
13
- readonly host: Group;
14
- readonly nodes: readonly AnyNode[];
15
- private readonly parts: BindingPart[] = [];
15
+ type Segment = AnyNode | ChildPart;
16
+ /** The nodes and child holes one template places in one parent, from index `base()`. */
17
+ interface Sequence {
18
+ parent: ParentNode;
19
+ readonly segments: Segment[];
20
+ base(): number;
21
+ }
22
+
23
+ const size = (segments: readonly Segment[]) =>
24
+ segments.reduce((total, segment) => total + (segment instanceof ChildPart ? segment.size : 1), 0);
25
+ const flat = (segments: readonly Segment[]): AnyNode[] =>
26
+ segments.flatMap((segment) => (segment instanceof ChildPart ? segment.nodes : [segment]));
27
+ /** Semantic disposal: unlike `destroy()`, undo can restore it and history isn't cleared. */
28
+ const dispose = (nodes: readonly AnyNode[]) => {
29
+ const live = nodes.filter((node) => !node.isDestroyed);
30
+ if (live.length) live[0][destroySubtrees](live);
31
+ };
32
+
33
+ /** Flattens a child hole's value into templates. */
34
+ function templates(value: TemplateValue, out: SvgTemplate[] = []): SvgTemplate[] {
35
+ if (value instanceof SvgTemplate) out.push(value);
36
+ else if (typeof value == 'object' && value && Symbol.iterator in value)
37
+ for (const item of value) templates(item, out);
38
+ else if (value != null && typeof value != 'boolean' && value !== '')
39
+ throw new TypeError('Child bindings take templates, iterables of them, or null');
40
+ return out;
41
+ }
42
+
43
+ /**
44
+ * Validates and builds an update without changing the drawing: new items are built in
45
+ * detached `stagings` groups. The returned commit applies it.
46
+ */
47
+ type Prepare = (values: readonly TemplateValue[], stagings: Group[]) => () => void;
48
+
49
+ /** Prepares and commits, disposing the staging groups whether or not it succeeds. */
50
+ function commit(prepare: Prepare, values: readonly TemplateValue[]): void {
51
+ const stagings: Group[] = [];
52
+ try {
53
+ prepare(values, stagings)();
54
+ } finally {
55
+ dispose(stagings);
56
+ }
57
+ }
58
+
59
+ /** A hole between elements: the items it renders, reused by index while their literal matches. */
60
+ class ChildPart {
61
+ private items: Mount[] = [];
62
+ constructor(
63
+ private readonly sequence: Sequence,
64
+ private readonly hole: number,
65
+ private readonly registry: Registry,
66
+ ) {}
67
+
68
+ get size(): number {
69
+ return this.items.reduce((total, item) => total + item.size, 0);
70
+ }
71
+ get nodes(): AnyNode[] {
72
+ return this.items.flatMap((item) => item.nodes);
73
+ }
74
+ /** The index of this part's first node, or of `item`'s first node. */
75
+ private start(item?: Mount): number {
76
+ const { segments } = this.sequence;
77
+ let index = this.sequence.base() + size(segments.slice(0, segments.indexOf(this)));
78
+ for (const other of item ? this.items : []) {
79
+ if (other === item) break;
80
+ index += other.size;
81
+ }
82
+ return index;
83
+ }
84
+
85
+ readonly prepare: Prepare = (values, stagings) => {
86
+ const parent = this.sequence.parent;
87
+ const forbidden = new Set<string>();
88
+ for (let node: AnyNode | null = parent; node; node = node.parent)
89
+ for (const tag of (node.constructor as ElementClass).forbiddenDescendantTags ?? [])
90
+ forbidden.add(tag);
91
+ const plans = templates(values[this.hole]).map((template, index) => {
92
+ const item = this.items[index];
93
+ if (item?.template.strings === template.strings)
94
+ return { item, commit: item.prepare(template.values, stagings) };
95
+ const content = TemplateCompiler.content(parent.context.dom, template.strings);
96
+ TemplateCompiler.validate(
97
+ content,
98
+ this.registry,
99
+ forbidden,
100
+ parent.constructor as ElementClass,
101
+ );
102
+ const staging = new Group(parent.context);
103
+ stagings.push(staging);
104
+ return { item: new Mount(template, staging, this.registry, content) };
105
+ });
106
+ return () => {
107
+ const items = plans.map((plan) => plan.item);
108
+ dispose(this.items.filter((item) => !items.includes(item)).flatMap((item) => item.nodes));
109
+ this.items = items;
110
+ for (const { item, commit } of plans)
111
+ if (!commit) {
112
+ // Detach first, so history records new nodes rather than a move from staging.
113
+ for (const node of item.nodes) parent.add(node.remove());
114
+ item.sequence.parent = parent;
115
+ item.sequence.base = () => this.start(item);
116
+ }
117
+ for (const plan of plans) plan.commit?.();
118
+ let index = this.start();
119
+ for (const node of this.nodes) parent.add(node, index++);
120
+ };
121
+ };
122
+ }
123
+
124
+ /** One template's nodes and parts, mounted as a sequence inside a parent. */
125
+ class Mount {
126
+ readonly sequence: Sequence;
127
+ private readonly parts: (BindingPart | ChildPart)[] = [];
16
128
 
17
129
  constructor(
18
- parent: ParentNode,
19
130
  readonly template: SvgTemplate,
20
- registry: Registry,
21
- descriptions: readonly TemplateDescription[],
131
+ parent: ParentNode,
132
+ private readonly registry: Registry,
133
+ content: readonly ContentItem[],
22
134
  ) {
23
- this.host = new Group(parent.context);
24
- try {
25
- this.nodes = descriptions.map((description) =>
26
- this.instantiate(description, this.host, registry),
27
- );
28
- } catch (error) {
29
- this.host.destroy();
30
- throw error;
31
- }
135
+ this.sequence = { parent, segments: [], base: () => 0 };
136
+ this.fill(content, this.sequence);
32
137
  }
33
138
 
34
- get isDestroyed(): boolean {
35
- return this.host.isDestroyed;
139
+ get size(): number {
140
+ return size(this.sequence.segments);
141
+ }
142
+ get nodes(): AnyNode[] {
143
+ return flat(this.sequence.segments);
36
144
  }
37
145
 
38
- update(template: SvgTemplate): this {
39
- return mutation(this, () => {
40
- this.host.assertLive();
41
- if (template.strings !== this.template.strings)
42
- throw new TypeError('An instance can only update its original template');
43
- const changes = this.parts.map((part) => {
44
- const value = part.evaluate(template.values);
45
- part.validate(value);
46
- return { part, value };
47
- });
48
- this.host.batch(() => {
49
- for (const { part, value } of changes) part.commit(value);
50
- });
51
- return this;
146
+ /** Validates every binding first, so an update changes all or nothing. */
147
+ readonly prepare: Prepare = (values, stagings) => {
148
+ const commits = this.parts.map((part) => {
149
+ if (part instanceof ChildPart) return part.prepare(values, stagings);
150
+ const value = part.evaluate(values);
151
+ part.validate(value);
152
+ return () => part.commit(value);
52
153
  });
53
- }
154
+ return () => commits.forEach((commit) => commit());
155
+ };
54
156
 
55
- destroy(): void {
56
- this.host.destroy();
157
+ private fill(content: readonly ContentItem[], sequence: Sequence): void {
158
+ for (const item of content) {
159
+ if ('tag' in item) sequence.segments.push(this.element(item, sequence.parent));
160
+ else
161
+ for (const hole of item.parts)
162
+ if (typeof hole == 'number') {
163
+ const part = new ChildPart(sequence, hole, this.registry);
164
+ sequence.segments.push(part);
165
+ this.parts.push(part);
166
+ commit(part.prepare, this.template.values);
167
+ }
168
+ }
57
169
  }
58
-
59
- private instantiate(
60
- description: TemplateDescription,
61
- parent: ParentNode,
62
- registry: Registry,
63
- ): AnyNode {
64
- const node = registry.createEmpty(description.tag, parent.context);
170
+ private element(description: TemplateDescription, parent: ParentNode): AnyNode {
171
+ const node = this.registry.createEmpty(description.tag, parent.context);
65
172
  parent.add(node);
66
173
  for (const [name, values] of Object.entries(description.attributes))
67
174
  this.bind(new BindingPart(node, name, values));
68
175
  if (appendTextSlot in node && Guards.isParent(node)) {
69
176
  for (const part of description.content) {
70
- if ('tag' in part) this.instantiate(part, node, registry);
177
+ if ('tag' in part) this.element(part, node);
71
178
  else {
72
179
  const binding = (node[appendTextSlot] as (value: string) => TextBinding)('');
73
180
  this.bind(new BindingPart(node, null, part.parts, binding));
@@ -75,22 +182,69 @@ export class TemplateInstance {
75
182
  }
76
183
  return node;
77
184
  }
78
- if (description.text.length > 0 && Guards.isTextual(node))
79
- this.bind(new BindingPart(node, null, description.text));
185
+ if (Guards.isTextual(node)) {
186
+ if (description.text.length > 0) this.bind(new BindingPart(node, null, description.text));
187
+ } else if (Guards.isParent(node)) {
188
+ this.fill(description.content, { parent: node, segments: [], base: () => 0 });
189
+ return node;
190
+ }
80
191
  if (description.children.length > 0 && !Guards.isParent(node))
81
192
  throw new TypeError('Children require the Parent mixin');
82
- for (const child of description.children) this.instantiate(child, node as ParentNode, registry);
193
+ for (const child of description.children) this.element(child, node as ParentNode);
83
194
  return node;
84
195
  }
85
196
 
86
197
  private bind(part: BindingPart): void {
87
198
  const value = part.evaluate(this.template.values);
88
199
  part.validate(value);
89
- part.commit(value, true);
200
+ part.commit(value);
90
201
  this.parts.push(part);
91
202
  }
92
203
  }
93
204
 
205
+ export class TemplateInstance {
206
+ readonly host: Group;
207
+ private readonly mount: Mount;
208
+
209
+ constructor(
210
+ parent: ParentNode,
211
+ readonly template: SvgTemplate,
212
+ registry: Registry,
213
+ content: readonly ContentItem[],
214
+ ) {
215
+ this.host = new Group(parent.context);
216
+ try {
217
+ this.mount = new Mount(template, this.host, registry, content);
218
+ } catch (error) {
219
+ this.host.destroy();
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ /** The current top-level nodes, including those that child holes render. */
225
+ get nodes(): readonly AnyNode[] {
226
+ return this.mount.nodes;
227
+ }
228
+
229
+ get isDestroyed(): boolean {
230
+ return this.host.isDestroyed;
231
+ }
232
+
233
+ update(template: SvgTemplate): this {
234
+ return mutation(this, () => {
235
+ this.host.assertLive();
236
+ if (template.strings !== this.template.strings)
237
+ throw new TypeError('An instance can only update its original template');
238
+ this.host.batch(() => commit(this.mount.prepare, template.values));
239
+ return this;
240
+ });
241
+ }
242
+
243
+ destroy(): void {
244
+ this.host.destroy();
245
+ }
246
+ }
247
+
94
248
  export class Renderer {
95
249
  private static readonly renderers = new WeakMap<ParentNode, Renderer>();
96
250
  private instance: TemplateInstance | null = null;
@@ -126,12 +280,15 @@ export class Renderer {
126
280
  this.instance.template.strings === template.strings
127
281
  )
128
282
  return this.instance.update(template);
129
- const descriptions = TemplateCompiler.compile(this.parent.context.dom, template.strings);
130
- TemplateCompiler.validate(descriptions, this.registry);
131
- const instance = new TemplateInstance(this.parent, template, this.registry, descriptions);
283
+ const content = TemplateCompiler.content(this.parent.context.dom, template.strings);
284
+ TemplateCompiler.validate(content, this.registry);
285
+ const instance = new TemplateInstance(this.parent, template, this.registry, content);
132
286
  try {
133
- this.instance?.destroy();
134
- this.parent.add(instance.host);
287
+ // Replacing a template disposes the old one semantically, so undo restores it.
288
+ this.parent.context.changes.run(this.parent, () => {
289
+ if (this.instance) dispose([this.instance.host]);
290
+ this.parent.add(instance.host);
291
+ });
135
292
  } catch (error) {
136
293
  instance.destroy();
137
294
  throw error;
@@ -1,4 +1,9 @@
1
- export type TemplateValue = string | number | boolean | null | undefined;
1
+ /**
2
+ * Attribute and text holes take primitives. A hole between elements also takes a nested
3
+ * template or an iterable of them; `null`, `undefined`, booleans and `''` render nothing.
4
+ */
5
+ export type TemplateValue =
6
+ string | number | boolean | null | undefined | SvgTemplate | Iterable<TemplateValue>;
2
7
 
3
8
  export class SvgTemplate {
4
9
  constructor(
@@ -1,6 +1,7 @@
1
1
  export const appendTextSlot = Symbol('appendTextSlot');
2
2
 
3
3
  export interface TextBinding {
4
+ readonly value: string;
4
5
  validate(): void;
5
6
  set(value: string): void;
6
7
  }
@@ -101,3 +101,32 @@ export interface Disposable {
101
101
  readonly isDestroyed: boolean;
102
102
  destroy(): void;
103
103
  }
104
+
105
+ /**
106
+ * Per-node editing limits that tools read structurally from `node.editRules`, for
107
+ * example from a component type. A node without rules edits freely.
108
+ */
109
+ export interface EditRules {
110
+ /** `'free'` (default), a positive step in degrees that divides 360, or `false`. */
111
+ readonly rotate?: 'free' | number | false;
112
+ /** Whether flips and mirroring resizes are allowed. Defaults to true. */
113
+ readonly flip?: boolean;
114
+ /**
115
+ * `'scale'` (default), `false`, or `'props'`: a resize changes the node's own width and
116
+ * height through `size()` instead of scaling it. Multi-node resizes don't apply to it.
117
+ */
118
+ readonly resize?: 'scale' | 'props' | false;
119
+ }
120
+
121
+ /**
122
+ * A named connection point that connectors read structurally from
123
+ * `node.portDefinitions`, for example from a component. `x` and `y` are in the node's
124
+ * local coordinates, before its own transform.
125
+ */
126
+ export interface PortDefinition {
127
+ readonly name: string;
128
+ readonly x: number;
129
+ readonly y: number;
130
+ /** The side a link leaves from, before the node's transform. Elbow routing uses it. */
131
+ readonly side?: 'left' | 'right' | 'top' | 'bottom';
132
+ }