@rasamjs/core 0.1.2 → 0.2.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 (106) hide show
  1. package/API.md +15 -5
  2. package/CHANGELOG.md +39 -1
  3. package/README.md +140 -20
  4. package/RELEASING.md +29 -21
  5. package/dist/changes/service.d.ts +5 -2
  6. package/dist/changes/service.d.ts.map +1 -1
  7. package/dist/changes/service.js +29 -4
  8. package/dist/changes/service.js.map +1 -1
  9. package/dist/changes/types.d.ts +9 -0
  10. package/dist/changes/types.d.ts.map +1 -1
  11. package/dist/changes/types.js.map +1 -1
  12. package/dist/changes.d.ts +5 -3
  13. package/dist/changes.d.ts.map +1 -1
  14. package/dist/changes.js +103 -2
  15. package/dist/changes.js.map +1 -1
  16. package/dist/dom/images.d.ts.map +1 -1
  17. package/dist/dom/images.js +3 -1
  18. package/dist/dom/images.js.map +1 -1
  19. package/dist/elements/image.d.ts.map +1 -1
  20. package/dist/elements/image.js +4 -1
  21. package/dist/elements/image.js.map +1 -1
  22. package/dist/interchange/clone.js +1 -1
  23. package/dist/interchange/clone.js.map +1 -1
  24. package/dist/interchange/dom.d.ts +3 -1
  25. package/dist/interchange/dom.d.ts.map +1 -1
  26. package/dist/interchange/dom.js +29 -5
  27. package/dist/interchange/dom.js.map +1 -1
  28. package/dist/interchange/markup.d.ts.map +1 -1
  29. package/dist/interchange/markup.js +71 -3
  30. package/dist/interchange/markup.js.map +1 -1
  31. package/dist/interchange/policy.d.ts.map +1 -1
  32. package/dist/interchange/policy.js +4 -0
  33. package/dist/interchange/policy.js.map +1 -1
  34. package/dist/interchange/types.d.ts +10 -1
  35. package/dist/interchange/types.d.ts.map +1 -1
  36. package/dist/mixins/transformable.d.ts.map +1 -1
  37. package/dist/mixins/transformable.js +25 -27
  38. package/dist/mixins/transformable.js.map +1 -1
  39. package/dist/raster/frame.d.ts +12 -0
  40. package/dist/raster/frame.d.ts.map +1 -0
  41. package/dist/raster/frame.js +43 -0
  42. package/dist/raster/frame.js.map +1 -0
  43. package/dist/raster/images.d.ts +2 -0
  44. package/dist/raster/images.d.ts.map +1 -0
  45. package/dist/raster/images.js +60 -0
  46. package/dist/raster/images.js.map +1 -0
  47. package/dist/raster/render.d.ts +3 -0
  48. package/dist/raster/render.d.ts.map +1 -0
  49. package/dist/raster/render.js +40 -0
  50. package/dist/raster/render.js.map +1 -0
  51. package/dist/raster/types.d.ts +10 -0
  52. package/dist/raster/types.d.ts.map +1 -0
  53. package/dist/raster/types.js +2 -0
  54. package/dist/raster/types.js.map +1 -0
  55. package/dist/raster.d.ts +6 -0
  56. package/dist/raster.d.ts.map +1 -0
  57. package/dist/raster.js +10 -0
  58. package/dist/raster.js.map +1 -0
  59. package/dist/resources/references.d.ts.map +1 -1
  60. package/dist/resources/references.js +3 -1
  61. package/dist/resources/references.js.map +1 -1
  62. package/dist/services/viewport.d.ts +2 -0
  63. package/dist/services/viewport.d.ts.map +1 -1
  64. package/dist/services/viewport.js +4 -3
  65. package/dist/services/viewport.js.map +1 -1
  66. package/dist/text/content.d.ts +0 -2
  67. package/dist/text/content.d.ts.map +1 -1
  68. package/dist/text/content.js +4 -10
  69. package/dist/text/content.js.map +1 -1
  70. package/dist/text/policies.d.ts +7 -0
  71. package/dist/text/policies.d.ts.map +1 -0
  72. package/dist/text/policies.js +13 -0
  73. package/dist/text/policies.js.map +1 -0
  74. package/package.json +6 -1
  75. package/reports/adoption.json +20 -20
  76. package/reports/animation-benchmark.json +617 -617
  77. package/reports/benchmark.json +44 -44
  78. package/reports/creation.json +26 -26
  79. package/reports/interchange-benchmark.json +85 -85
  80. package/reports/public-api-contract.json +20 -7
  81. package/reports/public-api-review.md +23 -0
  82. package/reports/size-parity.json +54 -19
  83. package/reports/size.json +2 -2
  84. package/reports/svgjs-comparison.json +3464 -3464
  85. package/reports/svgjs-comparison.md +19 -19
  86. package/reports/svgjs-parity-current.json +58 -34
  87. package/src/changes/service.ts +39 -5
  88. package/src/changes/types.ts +9 -0
  89. package/src/changes.ts +93 -4
  90. package/src/dom/images.ts +2 -1
  91. package/src/elements/image.ts +4 -1
  92. package/src/interchange/clone.ts +1 -1
  93. package/src/interchange/dom.ts +34 -5
  94. package/src/interchange/markup.ts +77 -3
  95. package/src/interchange/policy.ts +6 -0
  96. package/src/interchange/types.ts +10 -1
  97. package/src/mixins/transformable.ts +23 -25
  98. package/src/raster/frame.ts +53 -0
  99. package/src/raster/images.ts +73 -0
  100. package/src/raster/render.ts +45 -0
  101. package/src/raster/types.ts +9 -0
  102. package/src/raster.ts +13 -0
  103. package/src/resources/references.ts +5 -1
  104. package/src/services/viewport.ts +5 -4
  105. package/src/text/content.ts +4 -14
  106. package/src/text/policies.ts +18 -0
@@ -1,29 +1,29 @@
1
1
  # Rasam vs SVG.js
2
2
 
3
- SVG.js 3.2.8; Rasam 0.1.2 workspace. CPU: Intel(R) Core(TM) i7-10610U CPU @ 1.80GHz.
3
+ SVG.js 3.2.8; Rasam 0.2.0 workspace. CPU: Intel(R) Core(TM) i7-10610U CPU @ 1.80GHz.
4
4
 
5
5
  All timings below are medians in milliseconds. Ratios above 1 mean Rasam took longer.
6
6
 
7
7
  | Browser | Workload | Nodes | Samples | Rasam | SVG.js | Rasam / SVG.js |
8
8
  | --- | --- | ---: | ---: | ---: | ---: | ---: |
9
- | chromium 156.0.8078.4 | creation | 2000 | 15 | 9.70 | 12.20 | 0.80 |
10
- | chromium 156.0.8078.4 | creation | 8000 | 15 | 36.50 | 45.10 | 0.81 |
11
- | chromium 156.0.8078.4 | fill | 1000 | 100 | 0.50 | 1.39 | 0.36 |
12
- | chromium 156.0.8078.4 | repeated-fill | 1000 | 100 | 0.71 | 4.23 | 0.17 |
13
- | chromium 156.0.8078.4 | pointer-one | 8000 | 40 | 4.00 | 3.82 | 1.05 |
14
- | chromium 156.0.8078.4 | pointer-all | 8000 | 40 | 4.02 | 3.80 | 1.06 |
15
- | firefox 157.0 | creation | 2000 | 15 | 23.00 | 25.00 | 0.92 |
16
- | firefox 157.0 | creation | 8000 | 15 | 93.00 | 85.00 | 1.09 |
17
- | firefox 157.0 | fill | 1000 | 100 | 1.20 | 1.60 | 0.75 |
18
- | firefox 157.0 | repeated-fill | 1000 | 100 | 1.70 | 5.00 | 0.34 |
19
- | firefox 157.0 | pointer-one | 8000 | 40 | 4.40 | 3.80 | 1.16 |
20
- | firefox 157.0 | pointer-all | 8000 | 40 | 4.60 | 3.80 | 1.21 |
21
- | webkit 27.2 | creation | 2000 | 15 | 15.00 | 20.00 | 0.75 |
22
- | webkit 27.2 | creation | 8000 | 15 | 56.00 | 73.00 | 0.77 |
23
- | webkit 27.2 | fill | 1000 | 100 | 0.70 | 1.80 | 0.39 |
24
- | webkit 27.2 | repeated-fill | 1000 | 100 | 1.35 | 6.85 | 0.20 |
25
- | webkit 27.2 | pointer-one | 8000 | 40 | 4.40 | 3.60 | 1.22 |
26
- | webkit 27.2 | pointer-all | 8000 | 40 | 4.20 | 3.60 | 1.17 |
9
+ | chromium 156.0.8078.4 | creation | 2000 | 15 | 9.60 | 11.90 | 0.81 |
10
+ | chromium 156.0.8078.4 | creation | 8000 | 15 | 33.80 | 38.70 | 0.87 |
11
+ | chromium 156.0.8078.4 | fill | 1000 | 100 | 0.43 | 1.21 | 0.36 |
12
+ | chromium 156.0.8078.4 | repeated-fill | 1000 | 100 | 0.66 | 3.53 | 0.19 |
13
+ | chromium 156.0.8078.4 | pointer-one | 8000 | 40 | 3.73 | 3.56 | 1.05 |
14
+ | chromium 156.0.8078.4 | pointer-all | 8000 | 40 | 3.82 | 3.66 | 1.04 |
15
+ | firefox 157.0 | creation | 2000 | 15 | 20.00 | 23.00 | 0.87 |
16
+ | firefox 157.0 | creation | 8000 | 15 | 75.00 | 80.00 | 0.94 |
17
+ | firefox 157.0 | fill | 1000 | 100 | 1.20 | 1.55 | 0.77 |
18
+ | firefox 157.0 | repeated-fill | 1000 | 100 | 1.60 | 4.70 | 0.34 |
19
+ | firefox 157.0 | pointer-one | 8000 | 40 | 4.30 | 3.60 | 1.19 |
20
+ | firefox 157.0 | pointer-all | 8000 | 40 | 4.20 | 3.60 | 1.17 |
21
+ | webkit 27.2 | creation | 2000 | 15 | 14.00 | 18.00 | 0.78 |
22
+ | webkit 27.2 | creation | 8000 | 15 | 55.00 | 69.00 | 0.80 |
23
+ | webkit 27.2 | fill | 1000 | 100 | 0.60 | 1.60 | 0.37 |
24
+ | webkit 27.2 | repeated-fill | 1000 | 100 | 0.90 | 4.90 | 0.18 |
25
+ | webkit 27.2 | pointer-one | 8000 | 40 | 4.20 | 3.40 | 1.24 |
26
+ | webkit 27.2 | pointer-all | 8000 | 40 | 4.20 | 3.40 | 1.24 |
27
27
 
28
28
  ## Methodology
29
29
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-11-02T00:13:40.368Z",
3
- "revision": "cf520e325957ab98952540b7234db122a672d9ed",
2
+ "generatedAt": "2026-11-06T20:46:53.699Z",
3
+ "revision": "a802f04c6f0965cedbd4c469fad996a445c6f878",
4
4
  "dirtyTree": [
5
5
  "M packages/rasam/reports/adoption.json",
6
6
  " M packages/rasam/reports/creation.json",
@@ -77,7 +77,7 @@
77
77
  },
78
78
  {
79
79
  "path": "src/changes.ts",
80
- "sha256": "ad8d700a86ca9417e551b6062f470c25cfa20dd445d9712cd9850c5129b3e29e"
80
+ "sha256": "3c1270b193852d88350c84c66453bba1601a4ec6da8263c20e7a4a482982738b"
81
81
  },
82
82
  {
83
83
  "path": "src/changes/mutation.ts",
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "path": "src/changes/service.ts",
92
- "sha256": "53369e7383275c12d3237ea5ce5939c1c072aa7bd870753971e14fd96f17b64d"
92
+ "sha256": "75d5347a4bce501bd2d691a1ead98767d9a8e7d9bb116777107689ee52abdc54"
93
93
  },
94
94
  {
95
95
  "path": "src/changes/state.ts",
@@ -97,7 +97,7 @@
97
97
  },
98
98
  {
99
99
  "path": "src/changes/types.ts",
100
- "sha256": "e951337de9f07ea31cdee2d34b4aa85f7b124545f0d53675d453f52d5798b224"
100
+ "sha256": "1158dc5615c62c5647cfbf3609ea7df1466819fa200b368e48a70259a30eec33"
101
101
  },
102
102
  {
103
103
  "path": "src/core/attribute-validation.ts",
@@ -157,7 +157,7 @@
157
157
  },
158
158
  {
159
159
  "path": "src/dom/images.ts",
160
- "sha256": "68f52ee7f43c78804c3dc4d66f0db9f1720ded54e3c51276359280c90ed615b3"
160
+ "sha256": "fb10a82529a22d639cc0422bc92b399c21545d8be622265515a76c89fa0f53f8"
161
161
  },
162
162
  {
163
163
  "path": "src/dom/namespace.ts",
@@ -217,7 +217,7 @@
217
217
  },
218
218
  {
219
219
  "path": "src/elements/image.ts",
220
- "sha256": "d5b5c79568d4fd60a81a3abed776f6a667fb61f4c04ae0aa4bd9905eedfef26f"
220
+ "sha256": "d9c96803619761a338365ced07720f1de77d02f1b64ddd4e8823d346cc766bb4"
221
221
  },
222
222
  {
223
223
  "path": "src/elements/index.ts",
@@ -365,19 +365,19 @@
365
365
  },
366
366
  {
367
367
  "path": "src/interchange/clone.ts",
368
- "sha256": "79c49807076752601058320ae20fe91c7d50037ee1d407bf999d4ed234a1f5b7"
368
+ "sha256": "ac5b0422f99912bd75117f6d25789ae7e86b858dee6662881a714be3aa1e21ad"
369
369
  },
370
370
  {
371
371
  "path": "src/interchange/dom.ts",
372
- "sha256": "3fe2ccf29a81f9ed29f4c960fce9d82e83b3d85297889fd0c755adc30a9cc8b0"
372
+ "sha256": "ca1eb93f3ed4466b941ce6f93e4ed8de30416674c5f3909c665b17c737432df4"
373
373
  },
374
374
  {
375
375
  "path": "src/interchange/markup.ts",
376
- "sha256": "cac5f9280764053e78964a7440fdee4c36dea7cef85e9d1fff5e86ef09cbe959"
376
+ "sha256": "18671962651a97a1f97188006dcb8e1df16aa1e02e39166459a59d6a3ec5a0fd"
377
377
  },
378
378
  {
379
379
  "path": "src/interchange/policy.ts",
380
- "sha256": "f6a50a9c7f292a08710e2d5416be741756be01397d7b79bc2af4f1d1339abe16"
380
+ "sha256": "917bc9b3476afe360ddceebbc467046538d80be9c3ba59f5bdabff97691b9445"
381
381
  },
382
382
  {
383
383
  "path": "src/interchange/reconstruct.ts",
@@ -393,7 +393,7 @@
393
393
  },
394
394
  {
395
395
  "path": "src/interchange/types.ts",
396
- "sha256": "59c02542c545be86a79b3352f2fb6d59ba28ebe4dc9b451d3bb641eaf3ffb157"
396
+ "sha256": "28745d333c1b7bb44414a305864d64fa252984f083efdaaa5f12eaa572120751"
397
397
  },
398
398
  {
399
399
  "path": "src/masks.ts",
@@ -485,7 +485,7 @@
485
485
  },
486
486
  {
487
487
  "path": "src/mixins/transformable.ts",
488
- "sha256": "958e6496e0e9de7ebc01da259ff1dbd1bb0e269550e055511390bbd3f7fb8f4c"
488
+ "sha256": "35d445e5069257ecd38551c6a468806875d862e61ac55d3f560df8079e84ab6b"
489
489
  },
490
490
  {
491
491
  "path": "src/mixins/types.ts",
@@ -511,6 +511,26 @@
511
511
  "path": "src/query.ts",
512
512
  "sha256": "d537a78cd1fa6668074a44eb7c41afb55b51986702a44b58c9e1ff9f1226ef57"
513
513
  },
514
+ {
515
+ "path": "src/raster.ts",
516
+ "sha256": "46df66cb6c18a66c892f82a46304ef6cb8cc144100766bb4e8eb34f6aa91f204"
517
+ },
518
+ {
519
+ "path": "src/raster/frame.ts",
520
+ "sha256": "908222caf5c93ee704fe078d39315177c4c635dc8f2cb39c2258f28f18bf2c15"
521
+ },
522
+ {
523
+ "path": "src/raster/images.ts",
524
+ "sha256": "417c5e907c6e223c6ee7c517850aa77c94173608acbae9d71ed798308b1ff5ee"
525
+ },
526
+ {
527
+ "path": "src/raster/render.ts",
528
+ "sha256": "ccc3cf1dbbecce83c3982b6fd59620a62ed422e6c2df6644af91959b2938b93f"
529
+ },
530
+ {
531
+ "path": "src/raster/types.ts",
532
+ "sha256": "60c9cb9ac8ed335c460434581411aa513bb9c10f8005e4ab20bc1bf721766b94"
533
+ },
514
534
  {
515
535
  "path": "src/resources.ts",
516
536
  "sha256": "9660bf7b369cb915521bd184f548ead723f9c0baa5243ea57d0d8a8993ab9f50"
@@ -521,7 +541,7 @@
521
541
  },
522
542
  {
523
543
  "path": "src/resources/references.ts",
524
- "sha256": "06c49b60b86254e3f8dbcda8d9ff4428e2bfd37c91bbde44cfaa621bc48647b2"
544
+ "sha256": "49a3aaba63aee99a725b67a85849a0f071c07e7f4d3b7458aa8cfc8409298895"
525
545
  },
526
546
  {
527
547
  "path": "src/resources/validation.ts",
@@ -565,7 +585,7 @@
565
585
  },
566
586
  {
567
587
  "path": "src/services/viewport.ts",
568
- "sha256": "f9297df66650cbc940c628c518072cc449e11594e9ea4c50ed27f700c32a8447"
588
+ "sha256": "b33adea566823e5afb7df79bc7d2eb1c074443422a1e1d8acecda64c956a455b"
569
589
  },
570
590
  {
571
591
  "path": "src/svg.ts",
@@ -589,7 +609,7 @@
589
609
  },
590
610
  {
591
611
  "path": "src/text/content.ts",
592
- "sha256": "ded249e165d537773cb1b91638feba3748e1f224779403f73be96bff053b2139"
612
+ "sha256": "99e3d11eec709b19b71ba7cf5ad720b60b317bd24b1272b404442e4597f6bb88"
593
613
  },
594
614
  {
595
615
  "path": "src/text/internals.ts",
@@ -599,6 +619,10 @@
599
619
  "path": "src/text/measure.ts",
600
620
  "sha256": "db023fce1361e9547adbc18040c87f5c524164fc97318322cf77d3c364ef78d2"
601
621
  },
622
+ {
623
+ "path": "src/text/policies.ts",
624
+ "sha256": "488f691c4047d53b05284c5224c501296c864a741e91aeedefdd5115a0f12b8e"
625
+ },
602
626
  {
603
627
  "path": "src/text/rich-textual.ts",
604
628
  "sha256": "52dce3a3bb91da57261dec67b8682c020bbe1ea0f22eea7aa1c02e386eb610d2"
@@ -652,8 +676,8 @@
652
676
  "sha256": "f14d89cb0695dc1c2958e831dd99b0e36cb103b0746af7bdec48c180bddf6a35"
653
677
  },
654
678
  {
655
- "path": "../../package-lock.json",
656
- "sha256": "a9244a1f25a62dc4fd328605dbb09b0639edaaa42939757fdfbccc7cdea963f6"
679
+ "path": "../../bun.lock",
680
+ "sha256": "d38780d5a3180ba7ee8c2cc6f6ff28c0065bb650496d78f55a3db94d5ce1800f"
657
681
  }
658
682
  ],
659
683
  "svgjs": "3.2.8",
@@ -9609,8 +9633,8 @@
9609
9633
  },
9610
9634
  "size": {
9611
9635
  "fixture": "fixtures/size.ts",
9612
- "rawBytes": 65967,
9613
- "gzipBytes": 19758,
9636
+ "rawBytes": 66378,
9637
+ "gzipBytes": 19897,
9614
9638
  "budgetBytes": 20480,
9615
9639
  "included": [
9616
9640
  "src/changes/mutation.ts",
@@ -9660,7 +9684,7 @@
9660
9684
  },
9661
9685
  "performance": {
9662
9686
  "fixture": "scripts/benchmark.mjs",
9663
- "date": "2026-11-02T00:13:34.754Z",
9687
+ "date": "2026-11-06T20:46:47.900Z",
9664
9688
  "browser": "Chromium 156.0.8078.4",
9665
9689
  "node": "v24.21.0",
9666
9690
  "os": "win32 10.0.26200",
@@ -9669,28 +9693,28 @@
9669
9693
  "count": 1000,
9670
9694
  "warmup": 20,
9671
9695
  "iterations": 100,
9672
- "medianMs": 0.5,
9673
- "p95Ms": 0.699999988079071,
9696
+ "medianMs": 0.6000000238418579,
9697
+ "p95Ms": 0.8000000715255737,
9674
9698
  "targetMedianMs": 16,
9675
9699
  "includes": "logical mutations and synchronous DOM commit; excludes paint",
9676
9700
  "timings": [
9677
- 0.3999999761581421, 0.3999999761581421, 0.3999999761581421, 0.3999999761581421,
9678
- 0.3999999761581421, 0.3999999761581421, 0.3999999761581421, 0.3999999761581421,
9679
- 0.3999999761581421, 0.3999999761581421, 0.40000003576278687, 0.40000003576278687,
9680
- 0.40000003576278687, 0.40000003576278687, 0.40000003576278687, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
9701
+ 0.3999999761581421, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
9681
9702
  0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
9682
- 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
9683
- 0.5999999642372131, 0.5999999642372131, 0.5999999642372131, 0.5999999642372131,
9684
- 0.5999999642372131, 0.5999999642372131, 0.5999999642372131, 0.5999999642372131,
9685
- 0.5999999642372131, 0.5999999642372131, 0.5999999642372131, 0.5999999642372131,
9703
+ 0.5, 0.5, 0.5999999046325684, 0.5999999046325684, 0.5999999046325684, 0.5999999046325684,
9704
+ 0.5999999046325684, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9705
+ 0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9706
+ 0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9686
9707
  0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9687
9708
  0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9688
9709
  0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9689
9710
  0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9690
9711
  0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579,
9691
- 0.699999988079071, 0.699999988079071, 0.699999988079071, 0.699999988079071, 0.699999988079071,
9692
- 0.7000000476837158, 0.7000000476837158, 0.800000011920929, 1.0999999642372131,
9693
- 1.399999976158142
9712
+ 0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.6999999284744263,
9713
+ 0.6999999284744263, 0.6999999284744263, 0.6999999284744263, 0.6999999284744263,
9714
+ 0.6999999284744263, 0.6999999284744263, 0.6999999284744263, 0.7000000476837158,
9715
+ 0.7000000476837158, 0.7000000476837158, 0.7000000476837158, 0.7000000476837158,
9716
+ 0.7000000476837158, 0.7000000476837158, 0.7000000476837158, 0.7999999523162842,
9717
+ 0.7999999523162842, 0.8000000715255737, 0.8000000715255737, 0.8999999761581421, 1, 1, 1
9694
9718
  ]
9695
9719
  }
9696
9720
  }
@@ -4,6 +4,7 @@ import type {
4
4
  ChangeEvent,
5
5
  ChangeGroup,
6
6
  ChangeMetadata,
7
+ ChangeDerivation,
7
8
  ModelChange,
8
9
  ModelSnapshot,
9
10
  NodeHandle,
@@ -18,6 +19,7 @@ interface ChangeAdapter {
18
19
 
19
20
  export class ChangeService {
20
21
  private readonly listeners = new Set<(event: ChangeEvent) => void>();
22
+ readonly derivations = new Set<(event: ChangeDerivation) => void>();
21
23
  private readonly identities = new WeakMap<AnyNode, NodeHandle>();
22
24
  private readonly retained = new WeakSet<AnyNode>();
23
25
  private readonly queue: ChangeEvent[] = [];
@@ -47,7 +49,7 @@ export class ChangeService {
47
49
  constructor(private readonly context: DrawingContext) {}
48
50
 
49
51
  get observed(): boolean {
50
- return this.listeners.size > 0;
52
+ return this.listeners.size > 0 || this.derivations.size > 0;
51
53
  }
52
54
  get replaying(): boolean {
53
55
  return this.replayDepth > 0;
@@ -194,6 +196,20 @@ export class ChangeService {
194
196
  try {
195
197
  if (node) this.touch(node);
196
198
  result = callback();
199
+ if (this.before.size && this.derivations.size) {
200
+ const nodes = [...this.before]
201
+ .filter(
202
+ ([target, previous]) =>
203
+ target.node.isDestroyed ||
204
+ this.adapter!.diff(target, previous, this.adapter!.capture(target.node)).length,
205
+ )
206
+ .map(([target]) => target.node);
207
+ if (nodes.length) {
208
+ const event = { ...this.metadata, nodes };
209
+ for (const derive of [...this.derivations])
210
+ if (this.derivations.has(derive)) derive(event);
211
+ }
212
+ }
197
213
  } catch (error) {
198
214
  failure = { error };
199
215
  }
@@ -266,13 +282,22 @@ export class ChangeService {
266
282
  }
267
283
  }
268
284
 
269
- group(metadata: ChangeMetadata = {}): ChangeGroup {
285
+ group(metadata: ChangeMetadata = {}): Omit<ChangeGroup, 'cancel'> & {
286
+ end(cancelled?: boolean, restored?: boolean): void;
287
+ } {
270
288
  const id = ++this.nextGroup,
271
289
  source = metadata.source ?? 'application';
272
290
  let active = true;
273
291
  this.groups.add(id);
274
292
  try {
275
- this.publish(Object.freeze({ type: 'groupstart', group: id, source }));
293
+ this.publish(
294
+ Object.freeze({
295
+ type: 'groupstart',
296
+ group: id,
297
+ source,
298
+ ...(metadata.label === undefined ? {} : { label: metadata.label }),
299
+ }),
300
+ );
276
301
  } catch (error) {
277
302
  this.groups.delete(id);
278
303
  this.publish(Object.freeze({ type: 'groupend', group: id, source, cancelled: true }));
@@ -284,11 +309,19 @@ export class ChangeService {
284
309
  if (!active) throw new Error('Change group has ended');
285
310
  return this.scope({ ...metadata, group: id }, callback, null);
286
311
  },
287
- end: (cancelled = false) => {
312
+ end: (cancelled = false, restored = false) => {
288
313
  if (!active) return;
289
314
  active = false;
290
315
  this.groups.delete(id);
291
- this.publish(Object.freeze({ type: 'groupend', group: id, source, cancelled }));
316
+ this.publish(
317
+ Object.freeze({
318
+ type: 'groupend',
319
+ group: id,
320
+ source,
321
+ cancelled,
322
+ ...(restored ? { restored: true } : {}),
323
+ }),
324
+ );
292
325
  },
293
326
  });
294
327
  }
@@ -378,6 +411,7 @@ export class ChangeService {
378
411
 
379
412
  destroy(): void {
380
413
  this.listeners.clear();
414
+ this.derivations.clear();
381
415
  this.queue.length = 0;
382
416
  this.notifications.length = 0;
383
417
  this.replacements.length = 0;
@@ -104,9 +104,14 @@ export type ModelChange =
104
104
 
105
105
  export interface ChangeMetadata {
106
106
  readonly source?: string;
107
+ readonly label?: string;
107
108
  readonly temporary?: boolean;
108
109
  readonly group?: number;
109
110
  }
111
+ /** Runs synchronously after an operation's writes, before its final change snapshot. */
112
+ export interface ChangeDerivation extends ChangeMetadata {
113
+ readonly nodes: readonly AnyNode[];
114
+ }
110
115
  export interface ChangeBatch {
111
116
  readonly type: 'change';
112
117
  readonly id: number;
@@ -126,7 +131,9 @@ export type ChangeEvent =
126
131
  readonly type: 'groupstart' | 'groupend';
127
132
  readonly group: number;
128
133
  readonly source: string;
134
+ readonly label?: string;
129
135
  readonly cancelled?: boolean;
136
+ readonly restored?: boolean;
130
137
  }
131
138
  | {
132
139
  readonly type: 'dispose' | 'willdispose';
@@ -136,6 +143,8 @@ export interface ChangeGroup {
136
143
  readonly id: number;
137
144
  run<Result>(callback: () => Result): Result;
138
145
  end(cancelled?: boolean): void;
146
+ /** Restore this group's own non-temporary changes, then end it as cancelled. */
147
+ cancel(): void;
139
148
  }
140
149
 
141
150
  export class ChangeConflictError extends Error {
package/src/changes.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  import type { AnyNode } from './core/types.js';
2
- import type { ChangeEvent, ChangeMetadata } from './changes/types.js';
3
- import { stateAdapter } from './changes/state.js';
2
+ import type { ChangeEvent, ChangeGroup, ChangeMetadata, ModelChange } from './changes/types.js';
3
+ import { ChangeConflictError, ChangeObserverError } from './changes/types.js';
4
+ import { combineChanges, stateAdapter } from './changes/state.js';
5
+ import { restoreChanges } from './changes/restore.js';
4
6
 
5
7
  export type {
6
8
  ChangeBatch,
7
9
  ChangeEvent,
8
10
  ChangeGroup,
9
11
  ChangeMetadata,
12
+ ChangeDerivation,
10
13
  ModelChange,
11
14
  ModelSnapshot,
12
15
  ModelFeatures,
@@ -35,6 +38,20 @@ export function observeChanges(
35
38
  drawing.assertLive();
36
39
  return drawing.context.changes.observe(listener, stateAdapter);
37
40
  }
41
+ /** Derived writes join the same operation and group. Callbacks never run during restoration. */
42
+ export function deriveChanges(
43
+ drawing: AnyNode,
44
+ derive: (event: import('./changes/types.js').ChangeDerivation) => void,
45
+ ): () => void {
46
+ drawing.assertLive();
47
+ if (typeof derive !== 'function') throw new TypeError('Change derivation must be a function');
48
+ drawing.context.changes.configure(stateAdapter);
49
+ const callbacks = drawing.context.changes.derivations;
50
+ callbacks.add(derive);
51
+ return () => {
52
+ callbacks.delete(derive);
53
+ };
54
+ }
38
55
  export function withChanges<Result>(
39
56
  drawing: AnyNode,
40
57
  metadata: ChangeMetadata,
@@ -43,9 +60,81 @@ export function withChanges<Result>(
43
60
  drawing.assertLive();
44
61
  return drawing.context.changes.scope(metadata, callback, drawing);
45
62
  }
46
- export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}) {
63
+ export function beginChangeGroup(drawing: AnyNode, metadata: ChangeMetadata = {}): ChangeGroup {
47
64
  drawing.assertLive();
48
- return drawing.context.changes.group(metadata);
65
+ const owner = drawing.context.root!;
66
+ let id: number | undefined;
67
+ let changes: readonly ModelChange[] = [];
68
+ let invalidated = false;
69
+ // Subscribe before groupstart so edits made by start observers are captured too.
70
+ const unsubscribe = observeChanges(drawing, (event) => {
71
+ if (event.type === 'dispose') invalidated = true;
72
+ if (id === undefined && event.type === 'groupstart') id = event.group;
73
+ if (event.type === 'change' && event.group === id && !event.temporary)
74
+ changes = combineChanges([...changes, ...event.changes]);
75
+ });
76
+ let group: ReturnType<typeof drawing.context.changes.group>;
77
+ try {
78
+ group = drawing.context.changes.group(metadata);
79
+ id = group.id;
80
+ } catch (error) {
81
+ unsubscribe();
82
+ throw error;
83
+ }
84
+ let active = true;
85
+ let cancelling = false;
86
+ let depth = 0;
87
+ const assertNotDisposed = () => {
88
+ if (invalidated)
89
+ throw new ChangeConflictError('Permanent disposal invalidated the change group');
90
+ };
91
+ const finish = (cancelled: boolean, restored = false) => {
92
+ active = false;
93
+ changes = [];
94
+ unsubscribe();
95
+ group.end(cancelled, restored);
96
+ };
97
+ return Object.freeze({
98
+ id: group.id,
99
+ run<Result>(callback: () => Result): Result {
100
+ if (!active || cancelling) throw new Error('Change group has ended or is cancelling');
101
+ assertNotDisposed();
102
+ depth++;
103
+ try {
104
+ return group.run(callback);
105
+ } finally {
106
+ depth--;
107
+ }
108
+ },
109
+ end(cancelled = false) {
110
+ if (active && !cancelling) finish(cancelled);
111
+ },
112
+ cancel() {
113
+ if (!active || cancelling) return;
114
+ if (depth) throw new Error('Finish the change callback before cancelling its group');
115
+ assertNotDisposed();
116
+ cancelling = true;
117
+ const failures: unknown[] = [];
118
+ try {
119
+ try {
120
+ if (changes.length) restoreChanges(owner, changes, 'undo');
121
+ } catch (error) {
122
+ if (error instanceof ChangeObserverError) failures.push(error);
123
+ else throw error;
124
+ }
125
+ try {
126
+ finish(true, true);
127
+ } catch (error) {
128
+ failures.push(error);
129
+ }
130
+ } finally {
131
+ cancelling = false;
132
+ }
133
+ if (failures.length === 1) throw failures[0];
134
+ if (failures.length)
135
+ throw new AggregateError(failures, 'Change group cancellation observers failed');
136
+ },
137
+ });
49
138
  }
50
139
 
51
140
  /** One recorder per context; ordinary change observers are unrestricted. */
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
- probe.removeAttribute('src');
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
  }
@@ -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(() => this.cancelRequest());
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
 
@@ -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/content.js';
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, () => {
@@ -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
- if (/<!\s*(?:DOCTYPE|ENTITY)|<\?/i.test(source))
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}">${source}</svg>`,
20
+ `<svg xmlns="${SVG_NAMESPACE}" xmlns:xlink="${XLINK_NAMESPACE}">${body}</svg>`,
14
21
  'image/svg+xml',
15
22
  );
16
- if (parsed.getElementsByTagName('parsererror').length)
17
- throw new TypeError('Invalid SVG markup');
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');