@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.
Files changed (240) hide show
  1. package/API.md +29 -9
  2. package/CHANGELOG.md +98 -1
  3. package/COMPONENTS.md +229 -0
  4. package/README.md +178 -25
  5. package/RELEASING.md +31 -21
  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 +10 -2
  19. package/dist/changes/service.d.ts.map +1 -1
  20. package/dist/changes/service.js +45 -6
  21. package/dist/changes/service.js.map +1 -1
  22. package/dist/changes/types.d.ts +9 -0
  23. package/dist/changes/types.d.ts.map +1 -1
  24. package/dist/changes/types.js.map +1 -1
  25. package/dist/changes.d.ts +5 -3
  26. package/dist/changes.d.ts.map +1 -1
  27. package/dist/changes.js +103 -2
  28. package/dist/changes.js.map +1 -1
  29. package/dist/components/changes.d.ts +34 -0
  30. package/dist/components/changes.d.ts.map +1 -0
  31. package/dist/components/changes.js +119 -0
  32. package/dist/components/changes.js.map +1 -0
  33. package/dist/components/component.d.ts +62 -0
  34. package/dist/components/component.d.ts.map +1 -0
  35. package/dist/components/component.js +355 -0
  36. package/dist/components/component.js.map +1 -0
  37. package/dist/components/decorators.d.ts +7 -0
  38. package/dist/components/decorators.d.ts.map +1 -0
  39. package/dist/components/decorators.js +81 -0
  40. package/dist/components/decorators.js.map +1 -0
  41. package/dist/components/metadata.d.ts +10 -0
  42. package/dist/components/metadata.d.ts.map +1 -0
  43. package/dist/components/metadata.js +85 -0
  44. package/dist/components/metadata.js.map +1 -0
  45. package/dist/components/registry.d.ts +24 -0
  46. package/dist/components/registry.d.ts.map +1 -0
  47. package/dist/components/registry.js +78 -0
  48. package/dist/components/registry.js.map +1 -0
  49. package/dist/components/types.d.ts +54 -0
  50. package/dist/components/types.d.ts.map +1 -0
  51. package/dist/components/types.js +2 -0
  52. package/dist/components/types.js.map +1 -0
  53. package/dist/components/view.d.ts +9 -0
  54. package/dist/components/view.d.ts.map +1 -0
  55. package/dist/components/view.js +104 -0
  56. package/dist/components/view.js.map +1 -0
  57. package/dist/components.d.ts +6 -0
  58. package/dist/components.d.ts.map +1 -0
  59. package/dist/components.js +5 -0
  60. package/dist/components.js.map +1 -0
  61. package/dist/core/node.d.ts +7 -0
  62. package/dist/core/node.d.ts.map +1 -1
  63. package/dist/core/node.js +30 -0
  64. package/dist/core/node.js.map +1 -1
  65. package/dist/dom/images.d.ts.map +1 -1
  66. package/dist/dom/images.js +3 -1
  67. package/dist/dom/images.js.map +1 -1
  68. package/dist/elements/image.d.ts.map +1 -1
  69. package/dist/elements/image.js +4 -1
  70. package/dist/elements/image.js.map +1 -1
  71. package/dist/interchange/clone.d.ts.map +1 -1
  72. package/dist/interchange/clone.js +4 -3
  73. package/dist/interchange/clone.js.map +1 -1
  74. package/dist/interchange/dom.d.ts +3 -1
  75. package/dist/interchange/dom.d.ts.map +1 -1
  76. package/dist/interchange/dom.js +29 -5
  77. package/dist/interchange/dom.js.map +1 -1
  78. package/dist/interchange/markup.d.ts.map +1 -1
  79. package/dist/interchange/markup.js +71 -3
  80. package/dist/interchange/markup.js.map +1 -1
  81. package/dist/interchange/policy.d.ts.map +1 -1
  82. package/dist/interchange/policy.js +4 -0
  83. package/dist/interchange/policy.js.map +1 -1
  84. package/dist/interchange/reconstruct.d.ts.map +1 -1
  85. package/dist/interchange/reconstruct.js +16 -12
  86. package/dist/interchange/reconstruct.js.map +1 -1
  87. package/dist/interchange/references.d.ts.map +1 -1
  88. package/dist/interchange/references.js +10 -4
  89. package/dist/interchange/references.js.map +1 -1
  90. package/dist/interchange/types.d.ts +10 -1
  91. package/dist/interchange/types.d.ts.map +1 -1
  92. package/dist/mixins/parent-internals.d.ts +6 -0
  93. package/dist/mixins/parent-internals.d.ts.map +1 -1
  94. package/dist/mixins/parent-internals.js +34 -0
  95. package/dist/mixins/parent-internals.js.map +1 -1
  96. package/dist/mixins/parent.d.ts.map +1 -1
  97. package/dist/mixins/parent.js +6 -1
  98. package/dist/mixins/parent.js.map +1 -1
  99. package/dist/mixins/transformable.d.ts.map +1 -1
  100. package/dist/mixins/transformable.js +25 -27
  101. package/dist/mixins/transformable.js.map +1 -1
  102. package/dist/mixins/types.d.ts +2 -2
  103. package/dist/mixins/types.d.ts.map +1 -1
  104. package/dist/raster/frame.d.ts +12 -0
  105. package/dist/raster/frame.d.ts.map +1 -0
  106. package/dist/raster/frame.js +43 -0
  107. package/dist/raster/frame.js.map +1 -0
  108. package/dist/raster/images.d.ts +2 -0
  109. package/dist/raster/images.d.ts.map +1 -0
  110. package/dist/raster/images.js +60 -0
  111. package/dist/raster/images.js.map +1 -0
  112. package/dist/raster/render.d.ts +3 -0
  113. package/dist/raster/render.d.ts.map +1 -0
  114. package/dist/raster/render.js +40 -0
  115. package/dist/raster/render.js.map +1 -0
  116. package/dist/raster/types.d.ts +10 -0
  117. package/dist/raster/types.d.ts.map +1 -0
  118. package/dist/raster/types.js +2 -0
  119. package/dist/raster/types.js.map +1 -0
  120. package/dist/raster.d.ts +6 -0
  121. package/dist/raster.d.ts.map +1 -0
  122. package/dist/raster.js +10 -0
  123. package/dist/raster.js.map +1 -0
  124. package/dist/resources/references.d.ts +2 -0
  125. package/dist/resources/references.d.ts.map +1 -1
  126. package/dist/resources/references.js +3 -1
  127. package/dist/resources/references.js.map +1 -1
  128. package/dist/services/catalog.d.ts.map +1 -1
  129. package/dist/services/catalog.js +5 -3
  130. package/dist/services/catalog.js.map +1 -1
  131. package/dist/services/defs-manager.d.ts +2 -0
  132. package/dist/services/defs-manager.d.ts.map +1 -1
  133. package/dist/services/defs-manager.js +4 -0
  134. package/dist/services/defs-manager.js.map +1 -1
  135. package/dist/services/element-key.d.ts +4 -0
  136. package/dist/services/element-key.d.ts.map +1 -0
  137. package/dist/services/element-key.js +12 -0
  138. package/dist/services/element-key.js.map +1 -0
  139. package/dist/services/initialization.d.ts +7 -0
  140. package/dist/services/initialization.d.ts.map +1 -0
  141. package/dist/services/initialization.js +27 -0
  142. package/dist/services/initialization.js.map +1 -0
  143. package/dist/services/registry.d.ts +8 -0
  144. package/dist/services/registry.d.ts.map +1 -1
  145. package/dist/services/registry.js +45 -10
  146. package/dist/services/registry.js.map +1 -1
  147. package/dist/services/types.d.ts +17 -0
  148. package/dist/services/types.d.ts.map +1 -1
  149. package/dist/services/viewport.d.ts +2 -0
  150. package/dist/services/viewport.d.ts.map +1 -1
  151. package/dist/services/viewport.js +4 -3
  152. package/dist/services/viewport.js.map +1 -1
  153. package/dist/template/parts.d.ts +12 -6
  154. package/dist/template/parts.d.ts.map +1 -1
  155. package/dist/template/parts.js +36 -29
  156. package/dist/template/parts.js.map +1 -1
  157. package/dist/template/render.d.ts +6 -7
  158. package/dist/template/render.d.ts.map +1 -1
  159. package/dist/template/render.js +188 -44
  160. package/dist/template/render.js.map +1 -1
  161. package/dist/template/svg-tag.d.ts +5 -1
  162. package/dist/template/svg-tag.d.ts.map +1 -1
  163. package/dist/template/svg-tag.js.map +1 -1
  164. package/dist/text/content.d.ts +0 -2
  165. package/dist/text/content.d.ts.map +1 -1
  166. package/dist/text/content.js +4 -10
  167. package/dist/text/content.js.map +1 -1
  168. package/dist/text/internals.d.ts +1 -0
  169. package/dist/text/internals.d.ts.map +1 -1
  170. package/dist/text/policies.d.ts +7 -0
  171. package/dist/text/policies.d.ts.map +1 -0
  172. package/dist/text/policies.js +13 -0
  173. package/dist/text/policies.js.map +1 -0
  174. package/dist/types/common.d.ts +27 -0
  175. package/dist/types/common.d.ts.map +1 -1
  176. package/package.json +156 -143
  177. package/reports/adoption.json +20 -20
  178. package/reports/animation-benchmark.json +616 -616
  179. package/reports/benchmark.json +65 -65
  180. package/reports/components-benchmark-before.json +10 -0
  181. package/reports/components-benchmark.json +10 -0
  182. package/reports/creation.json +26 -26
  183. package/reports/interchange-benchmark.json +85 -85
  184. package/reports/public-api-contract.json +353 -23
  185. package/reports/public-api-review.md +23 -0
  186. package/reports/size-components.json +102 -0
  187. package/reports/size-parity.json +82 -34
  188. package/reports/size.json +6 -4
  189. package/reports/svgjs-comparison.json +3466 -3466
  190. package/reports/svgjs-comparison.md +19 -19
  191. package/reports/svgjs-parity-current.json +177 -60
  192. package/src/advanced.ts +1 -1
  193. package/src/catalog/fragment.ts +2 -2
  194. package/src/changes/derived.ts +19 -0
  195. package/src/changes/restore.ts +27 -4
  196. package/src/changes/service.ts +60 -11
  197. package/src/changes/types.ts +9 -0
  198. package/src/changes.ts +93 -4
  199. package/src/components/changes.ts +131 -0
  200. package/src/components/component.ts +401 -0
  201. package/src/components/decorators.ts +104 -0
  202. package/src/components/metadata.ts +111 -0
  203. package/src/components/registry.ts +79 -0
  204. package/src/components/types.ts +66 -0
  205. package/src/components/view.ts +123 -0
  206. package/src/components.ts +15 -0
  207. package/src/core/node.ts +32 -1
  208. package/src/dom/images.ts +2 -1
  209. package/src/elements/image.ts +4 -1
  210. package/src/interchange/clone.ts +4 -3
  211. package/src/interchange/dom.ts +34 -5
  212. package/src/interchange/markup.ts +77 -3
  213. package/src/interchange/policy.ts +6 -0
  214. package/src/interchange/reconstruct.ts +16 -12
  215. package/src/interchange/references.ts +12 -4
  216. package/src/interchange/types.ts +10 -1
  217. package/src/mixins/parent-internals.ts +37 -0
  218. package/src/mixins/parent.ts +13 -4
  219. package/src/mixins/transformable.ts +23 -25
  220. package/src/mixins/types.ts +2 -2
  221. package/src/raster/frame.ts +53 -0
  222. package/src/raster/images.ts +73 -0
  223. package/src/raster/render.ts +45 -0
  224. package/src/raster/types.ts +9 -0
  225. package/src/raster.ts +13 -0
  226. package/src/resources/references.ts +5 -1
  227. package/src/services/catalog.ts +5 -3
  228. package/src/services/defs-manager.ts +5 -0
  229. package/src/services/element-key.ts +14 -0
  230. package/src/services/initialization.ts +25 -0
  231. package/src/services/registry.ts +47 -10
  232. package/src/services/types.ts +21 -0
  233. package/src/services/viewport.ts +5 -4
  234. package/src/template/parts.ts +54 -39
  235. package/src/template/render.ts +211 -54
  236. package/src/template/svg-tag.ts +6 -1
  237. package/src/text/content.ts +4 -14
  238. package/src/text/internals.ts +1 -0
  239. package/src/text/policies.ts +18 -0
  240. 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 {
@@ -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 { assertCanAddChildren, validateChild, validateIndex } from './parent-internals.js';
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
- return mutation(this, () => {
114
- this.assertLive();
115
- if (state !== undefined) return this.restoreTransformState(state);
116
- const { base, geometry } = this.mappingState();
117
- const transform = this.getAttributeValue('transform');
118
- const common = {
119
- tag: this.context.dom.tag(this.el),
120
- transform: transform === undefined ? null : String(transform),
121
- geometry: Object.freeze(geometry.toArray()),
122
- attributes: geometrySnapshot(this),
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: 'components',
134
- position: components.position,
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
 
@@ -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
  }
@@ -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 previous = pending.get(Constructor.tag) ?? registry.get(Constructor.tag);
9
+ const key = elementKey(Constructor);
10
+ const previous = pending.get(key) ?? registry.registered(Constructor);
9
11
  if (previous && previous !== Constructor)
10
- throw new Error(`Tag already registered: ${Constructor.tag}`);
11
- pending.set(Constructor.tag, Constructor);
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
+ }
@@ -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 previous = this.constructors.get(Constructor.tag);
27
+ const key = elementKey(Constructor);
28
+ const previous = this.constructors.get(key);
23
29
  if (previous === Constructor) return this;
24
- if (previous && previous !== Constructor)
25
- throw new Error(`Tag already registered: ${Constructor.tag}`);
26
- this.constructors.set(Constructor.tag, Constructor);
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.get(this.context.dom.tag(ancestor));
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
- const wrapped = this.wrapTree(element);
87
- return parent ? parent.add(wrapped) : wrapped;
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.get(tag);
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.get(this.context.dom.tag(element));
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.get(this.context.dom.tag(element));
179
+ const Constructor = this.resolve(element);
143
180
  if (Constructor?.opaqueContent) return [];
144
181
  return this.context.dom
145
182
  .children(element)
@@ -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;