domotion-svg 0.1.1

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 (119) hide show
  1. package/FEATURES.md +102 -0
  2. package/LICENSE +21 -0
  3. package/README.md +66 -0
  4. package/dist/animator.d.ts +158 -0
  5. package/dist/animator.js +424 -0
  6. package/dist/animator.test.d.ts +5 -0
  7. package/dist/animator.test.js +169 -0
  8. package/dist/border-radius.test.d.ts +1 -0
  9. package/dist/border-radius.test.js +148 -0
  10. package/dist/capture.d.ts +193 -0
  11. package/dist/capture.js +786 -0
  12. package/dist/chrome.d.ts +45 -0
  13. package/dist/chrome.js +107 -0
  14. package/dist/cli.d.ts +16 -0
  15. package/dist/cli.js +512 -0
  16. package/dist/client/dom.d.ts +10 -0
  17. package/dist/client/dom.js +17 -0
  18. package/dist/conic-raster.d.ts +58 -0
  19. package/dist/conic-raster.js +292 -0
  20. package/dist/conic-raster.test.d.ts +1 -0
  21. package/dist/conic-raster.test.js +187 -0
  22. package/dist/coretext-extractor.test.d.ts +1 -0
  23. package/dist/coretext-extractor.test.js +94 -0
  24. package/dist/coretext-helper.d.ts +60 -0
  25. package/dist/coretext-helper.js +205 -0
  26. package/dist/cross-origin-font-face.test.d.ts +1 -0
  27. package/dist/cross-origin-font-face.test.js +107 -0
  28. package/dist/cursor-overlay.d.ts +123 -0
  29. package/dist/cursor-overlay.js +207 -0
  30. package/dist/cursor-overlay.test.d.ts +1 -0
  31. package/dist/cursor-overlay.test.js +88 -0
  32. package/dist/dark-mode-capture.test.d.ts +1 -0
  33. package/dist/dark-mode-capture.test.js +158 -0
  34. package/dist/dark-mode-form-controls.test.d.ts +1 -0
  35. package/dist/dark-mode-form-controls.test.js +218 -0
  36. package/dist/dom-to-svg.d.ts +1016 -0
  37. package/dist/dom-to-svg.js +7717 -0
  38. package/dist/embed-remote-images.test.d.ts +1 -0
  39. package/dist/embed-remote-images.test.js +424 -0
  40. package/dist/form-controls.d.ts +70 -0
  41. package/dist/form-controls.js +1151 -0
  42. package/dist/frame-merge.d.ts +95 -0
  43. package/dist/frame-merge.js +374 -0
  44. package/dist/frame-merge.test.d.ts +6 -0
  45. package/dist/frame-merge.test.js +144 -0
  46. package/dist/gradients.d.ts +184 -0
  47. package/dist/gradients.js +937 -0
  48. package/dist/gradients.test.d.ts +1 -0
  49. package/dist/gradients.test.js +150 -0
  50. package/dist/index.d.ts +12 -0
  51. package/dist/index.js +7 -0
  52. package/dist/jsx-runtime.d.ts +27 -0
  53. package/dist/jsx-runtime.js +96 -0
  54. package/dist/jsx-runtime.test.d.ts +1 -0
  55. package/dist/jsx-runtime.test.js +41 -0
  56. package/dist/kerfjs-imports.test.d.ts +1 -0
  57. package/dist/kerfjs-imports.test.js +36 -0
  58. package/dist/mask.test.d.ts +1 -0
  59. package/dist/mask.test.js +206 -0
  60. package/dist/optimize.d.ts +12 -0
  61. package/dist/optimize.js +32 -0
  62. package/dist/preserve-aspect-ratio.test.d.ts +1 -0
  63. package/dist/preserve-aspect-ratio.test.js +38 -0
  64. package/dist/resize-embedded-images.d.ts +33 -0
  65. package/dist/resize-embedded-images.js +164 -0
  66. package/dist/resize-embedded-images.test.d.ts +9 -0
  67. package/dist/resize-embedded-images.test.js +255 -0
  68. package/dist/stacking-context.test.d.ts +1 -0
  69. package/dist/stacking-context.test.js +927 -0
  70. package/dist/text-renderer.d.ts +42 -0
  71. package/dist/text-renderer.js +608 -0
  72. package/dist/text-renderer.test.d.ts +1 -0
  73. package/dist/text-renderer.test.js +150 -0
  74. package/dist/text-to-path.d.ts +265 -0
  75. package/dist/text-to-path.js +1800 -0
  76. package/dist/text-to-path.test.d.ts +1 -0
  77. package/dist/text-to-path.test.js +570 -0
  78. package/dist/utils/escapeHtml.d.ts +2 -0
  79. package/dist/utils/escapeHtml.js +15 -0
  80. package/dist/webfont-unicode-range.test.d.ts +1 -0
  81. package/dist/webfont-unicode-range.test.js +174 -0
  82. package/package.json +55 -0
  83. package/src/animator.test.ts +179 -0
  84. package/src/animator.ts +660 -0
  85. package/src/border-radius.test.ts +160 -0
  86. package/src/capture.ts +810 -0
  87. package/src/cli.ts +582 -0
  88. package/src/conic-raster.test.ts +213 -0
  89. package/src/conic-raster.ts +309 -0
  90. package/src/coretext-extractor.test.ts +130 -0
  91. package/src/coretext-helper.ts +256 -0
  92. package/src/cross-origin-font-face.test.ts +119 -0
  93. package/src/cursor-overlay.test.ts +95 -0
  94. package/src/cursor-overlay.ts +297 -0
  95. package/src/dark-mode-capture.test.ts +177 -0
  96. package/src/dark-mode-form-controls.test.ts +228 -0
  97. package/src/dom-to-svg.ts +8376 -0
  98. package/src/embed-remote-images.test.ts +461 -0
  99. package/src/form-controls.ts +1174 -0
  100. package/src/frame-merge.test.ts +157 -0
  101. package/src/frame-merge.ts +447 -0
  102. package/src/globals.d.ts +2 -0
  103. package/src/gradients.test.ts +175 -0
  104. package/src/gradients.ts +955 -0
  105. package/src/index.ts +12 -0
  106. package/src/kerf-jsx-augmentation.d.ts +36 -0
  107. package/src/kerfjs-imports.test.tsx +45 -0
  108. package/src/mask.test.ts +274 -0
  109. package/src/optimize.ts +34 -0
  110. package/src/preserve-aspect-ratio.test.ts +49 -0
  111. package/src/resize-embedded-images.test.ts +292 -0
  112. package/src/resize-embedded-images.ts +180 -0
  113. package/src/stacking-context.test.ts +967 -0
  114. package/src/text-renderer.test.ts +162 -0
  115. package/src/text-renderer.ts +623 -0
  116. package/src/text-to-path.test.ts +639 -0
  117. package/src/text-to-path.ts +1810 -0
  118. package/src/utils/escapeHtml.ts +16 -0
  119. package/src/webfont-unicode-range.test.ts +207 -0
@@ -0,0 +1,16 @@
1
+ export function escapeHtml(str: string): string {
2
+ return str
3
+ .replace(/&/g, "&")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/"/g, "&quot;");
7
+ }
8
+
9
+ export function escapeAttr(str: string): string {
10
+ return str
11
+ .replace(/&/g, "&amp;")
12
+ .replace(/"/g, "&quot;")
13
+ .replace(/'/g, "&#39;")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;");
16
+ }
@@ -0,0 +1,207 @@
1
+ import * as fs from "fs";
2
+ import { describe, expect, it, beforeEach } from "vitest";
3
+ import { parseUnicodeRangeDescriptor } from "./capture.js";
4
+ import { __pickWebfontVariantMetaForCodepointForTest, __pickWebfontVariantMetaForTest, clearWebfonts, registerWebfont, unicodeRangeCovers } from "./text-to-path.js";
5
+
6
+ // DM-517: webfont registration honors the `@font-face { unicode-range: ... }`
7
+ // descriptor. Google-Fonts-style partitioning declares the same `(family,
8
+ // weight, style)` pair across multiple `@font-face` rules, each with a
9
+ // distinct `unicode-range` (Latin, Latin Ext, Cyrillic, Greek, …). Without
10
+ // honoring the descriptor, `pickWebfontVariant` may return the Cyrillic-only
11
+ // partition for a Latin run — the run lays out as .notdef tofu.
12
+
13
+ describe("parseUnicodeRangeDescriptor", () => {
14
+ it("returns undefined for empty / whitespace input", () => {
15
+ expect(parseUnicodeRangeDescriptor("")).toBeUndefined();
16
+ expect(parseUnicodeRangeDescriptor(" ")).toBeUndefined();
17
+ });
18
+
19
+ it("parses single codepoints (U+26)", () => {
20
+ expect(parseUnicodeRangeDescriptor("U+26")).toEqual([[0x26, 0x26]]);
21
+ });
22
+
23
+ it("parses interval forms (U+0-7F)", () => {
24
+ expect(parseUnicodeRangeDescriptor("U+0-7F")).toEqual([[0x0, 0x7f]]);
25
+ expect(parseUnicodeRangeDescriptor("U+0000-00FF")).toEqual([[0x0, 0xff]]);
26
+ });
27
+
28
+ it("parses wildcard forms (U+4??)", () => {
29
+ expect(parseUnicodeRangeDescriptor("U+4??")).toEqual([[0x400, 0x4ff]]);
30
+ expect(parseUnicodeRangeDescriptor("U+1F??")).toEqual([[0x1f00, 0x1fff]]);
31
+ });
32
+
33
+ it("parses comma-separated mixed forms (real Google Fonts Cyrillic partition)", () => {
34
+ const ranges = parseUnicodeRangeDescriptor("U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116");
35
+ expect(ranges).toEqual([
36
+ [0x0301, 0x0301],
37
+ [0x0400, 0x045f],
38
+ [0x0490, 0x0491],
39
+ [0x04b0, 0x04b1],
40
+ [0x2116, 0x2116],
41
+ ]);
42
+ });
43
+
44
+ it("is case-insensitive on the U+ prefix", () => {
45
+ expect(parseUnicodeRangeDescriptor("u+0-7f")).toEqual([[0x0, 0x7f]]);
46
+ });
47
+ });
48
+
49
+ describe("unicodeRangeCovers", () => {
50
+ it("returns true for any codepoint when ranges is undefined (CSS default)", () => {
51
+ expect(unicodeRangeCovers(undefined, 0x0041)).toBe(true);
52
+ expect(unicodeRangeCovers(undefined, 0x10ffff)).toBe(true);
53
+ });
54
+
55
+ it("returns true for codepoints inside any interval", () => {
56
+ const ranges: Array<[number, number]> = [[0x0, 0x7f], [0x0400, 0x04ff]];
57
+ expect(unicodeRangeCovers(ranges, 0x0041)).toBe(true); // 'A' in Basic Latin
58
+ expect(unicodeRangeCovers(ranges, 0x0410)).toBe(true); // Cyrillic 'А'
59
+ });
60
+
61
+ it("returns false for codepoints outside all intervals", () => {
62
+ const ranges: Array<[number, number]> = [[0x0400, 0x04ff]]; // Cyrillic only
63
+ expect(unicodeRangeCovers(ranges, 0x0041)).toBe(false); // 'A' Latin — not covered
64
+ expect(unicodeRangeCovers(ranges, 0x4e00)).toBe(false); // CJK — not covered
65
+ });
66
+
67
+ it("respects interval boundaries (inclusive)", () => {
68
+ const ranges: Array<[number, number]> = [[0x0020, 0x007f]];
69
+ expect(unicodeRangeCovers(ranges, 0x001f)).toBe(false);
70
+ expect(unicodeRangeCovers(ranges, 0x0020)).toBe(true);
71
+ expect(unicodeRangeCovers(ranges, 0x007f)).toBe(true);
72
+ expect(unicodeRangeCovers(ranges, 0x0080)).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe("pickWebfontVariant: unicode-range preference (DM-517)", () => {
77
+ // Use a real system font buffer — `registerWebfont` calls `fontkit.create`
78
+ // and silently skips on parse failure, so we can't fake the buffer.
79
+ const helveticaBuf = fs.existsSync("/System/Library/Fonts/Helvetica.ttc")
80
+ ? fs.readFileSync("/System/Library/Fonts/Helvetica.ttc")
81
+ : null;
82
+ const haveFontFixture = helveticaBuf != null;
83
+
84
+ beforeEach(() => {
85
+ clearWebfonts();
86
+ });
87
+
88
+ it.skipIf(!haveFontFixture)("prefers Latin-covering variant when ties on weight + italic (Cyrillic-first registration order)", () => {
89
+ // Simulates Google Fonts' Geist@400 partitioning: Cyrillic partition
90
+ // registers first, then the Latin partition. Without unicode-range
91
+ // awareness, `pickWebfontVariant` would return the Cyrillic variant for
92
+ // a request like (geist, 400, italic=false) because it scores first.
93
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0301, 0x0301], [0x0400, 0x045f]]);
94
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0000, 0x00ff], [0x0131, 0x0131]]);
95
+
96
+ const picked = __pickWebfontVariantMetaForTest("geist", 400, false);
97
+ expect(picked).not.toBeNull();
98
+ // The Latin-covering partition (second registration) must win.
99
+ expect(picked!.unicodeRange).toEqual([[0x0000, 0x00ff], [0x0131, 0x0131]]);
100
+ });
101
+
102
+ it.skipIf(!haveFontFixture)("variant with no unicode-range (CSS default = U+0..U+10FFFF) ties non-partitioned cases unchanged", () => {
103
+ // Single registration, no unicode-range — most common case (single woff2
104
+ // covering everything). Behavior unchanged from pre-DM-517.
105
+ registerWebfont("inter", 500, "normal", helveticaBuf!);
106
+ const picked = __pickWebfontVariantMetaForTest("inter", 500, false);
107
+ expect(picked).not.toBeNull();
108
+ expect(picked!.unicodeRange).toBeUndefined();
109
+ expect(picked!.weight).toBe(500);
110
+ });
111
+
112
+ it.skipIf(!haveFontFixture)("range coverage outweighs italic mismatch — synthesized italic on Latin font beats tofu from italic Cyrillic font", () => {
113
+ // Pathological registration: italic Cyrillic-only + upright Latin-only.
114
+ // Asked for italic, the picker should still prefer the Latin variant
115
+ // because rendering tofu (.notdef) is far worse than synthesized italic.
116
+ registerWebfont("geist", 400, "italic", helveticaBuf!, [[0x0400, 0x045f]]);
117
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0000, 0x00ff]]);
118
+
119
+ const picked = __pickWebfontVariantMetaForTest("geist", 400, true);
120
+ expect(picked).not.toBeNull();
121
+ // Upright Latin-covering variant wins despite italic mismatch.
122
+ expect(picked!.italic).toBe(false);
123
+ expect(picked!.unicodeRange).toEqual([[0x0000, 0x00ff]]);
124
+ });
125
+
126
+ it.skipIf(!haveFontFixture)("when only non-Latin partitions are registered, picker returns the closest by weight (last-resort fallback)", () => {
127
+ // No Latin-covering variant available — picker still returns the best
128
+ // available (weight match). Avoids returning null for pages where only
129
+ // non-Latin partitions were fetched.
130
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0400, 0x045f]]);
131
+ registerWebfont("geist", 700, "normal", helveticaBuf!, [[0x0400, 0x045f]]);
132
+
133
+ const picked = __pickWebfontVariantMetaForTest("geist", 400, false);
134
+ expect(picked).not.toBeNull();
135
+ expect(picked!.weight).toBe(400);
136
+ });
137
+ });
138
+
139
+ describe("pickWebfontVariantForCodepoint: per-codepoint partition routing (DM-557)", () => {
140
+ // Run-splitter calls this for codepoints the primary (Latin-biased)
141
+ // variant can't shape. The function filters variants by `unicode-range`
142
+ // coverage of the codepoint, then scores remaining variants by italic +
143
+ // weight match.
144
+ const helveticaBuf = require("fs").existsSync("/System/Library/Fonts/Helvetica.ttc")
145
+ ? require("fs").readFileSync("/System/Library/Fonts/Helvetica.ttc") : null;
146
+ const haveFontFixture = helveticaBuf != null;
147
+
148
+ beforeEach(() => {
149
+ clearWebfonts();
150
+ });
151
+
152
+ it.skipIf(!haveFontFixture)("returns the variant whose unicode-range covers the requested codepoint", () => {
153
+ // Multi-partition Geist: Latin + Cyrillic + Latin-Ext partitions all
154
+ // registered at weight=400. Asking for codepoint U+0410 (Cyrillic 'А')
155
+ // must return the Cyrillic partition.
156
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0000, 0x00ff]]); // Latin
157
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0400, 0x045f]]); // Cyrillic
158
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0100, 0x017f]]); // Latin Ext
159
+
160
+ const picked = __pickWebfontVariantMetaForCodepointForTest("geist", 400, false, 0x0410);
161
+ expect(picked).not.toBeNull();
162
+ expect(picked!.unicodeRange).toEqual([[0x0400, 0x045f]]);
163
+ });
164
+
165
+ it.skipIf(!haveFontFixture)("returns the Latin partition for ASCII codepoints", () => {
166
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0000, 0x00ff]]);
167
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0400, 0x045f]]);
168
+
169
+ const picked = __pickWebfontVariantMetaForCodepointForTest("geist", 400, false, 0x0041); // 'A'
170
+ expect(picked).not.toBeNull();
171
+ expect(picked!.unicodeRange).toEqual([[0x0000, 0x00ff]]);
172
+ });
173
+
174
+ it.skipIf(!haveFontFixture)("returns null when no registered variant covers the codepoint", () => {
175
+ // Only Cyrillic registered; ask for a CJK codepoint outside any range.
176
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0400, 0x045f]]);
177
+
178
+ const picked = __pickWebfontVariantMetaForCodepointForTest("geist", 400, false, 0x4e00); // CJK
179
+ expect(picked).toBeNull();
180
+ });
181
+
182
+ it.skipIf(!haveFontFixture)("variant with no unicode-range covers all codepoints (CSS default)", () => {
183
+ // Single non-partitioned registration; should match any codepoint.
184
+ registerWebfont("inter", 400, "normal", helveticaBuf!);
185
+
186
+ const picked = __pickWebfontVariantMetaForCodepointForTest("inter", 400, false, 0x4e00);
187
+ expect(picked).not.toBeNull();
188
+ expect(picked!.unicodeRange).toBeUndefined();
189
+ });
190
+
191
+ it.skipIf(!haveFontFixture)("scores by italic + weight when multiple variants cover the codepoint", () => {
192
+ // Two italic Latin variants at weights 400 and 700; ask for italic 600.
193
+ // Should pick weight=700 (closer to 600 than 400).
194
+ registerWebfont("geist", 400, "italic", helveticaBuf!, [[0x0000, 0x00ff]]);
195
+ registerWebfont("geist", 700, "italic", helveticaBuf!, [[0x0000, 0x00ff]]);
196
+ registerWebfont("geist", 400, "normal", helveticaBuf!, [[0x0000, 0x00ff]]);
197
+
198
+ const picked = __pickWebfontVariantMetaForCodepointForTest("geist", 600, true, 0x0041);
199
+ expect(picked).not.toBeNull();
200
+ expect(picked!.italic).toBe(true);
201
+ expect(picked!.weight).toBe(700);
202
+ });
203
+
204
+ it.skipIf(!haveFontFixture)("returns null when family is not registered at all", () => {
205
+ expect(__pickWebfontVariantMetaForCodepointForTest("nonexistent", 400, false, 0x0041)).toBeNull();
206
+ });
207
+ });