@graphrefly/graphrefly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/chunk-5X3LAO3B.js +1571 -0
- package/dist/chunk-5X3LAO3B.js.map +1 -0
- package/dist/chunk-6W5SGIGB.js +1793 -0
- package/dist/chunk-6W5SGIGB.js.map +1 -0
- package/dist/chunk-CP6MNKAA.js +97 -0
- package/dist/chunk-CP6MNKAA.js.map +1 -0
- package/dist/chunk-HP7OKEOE.js +107 -0
- package/dist/chunk-HP7OKEOE.js.map +1 -0
- package/dist/chunk-KWXPDASV.js +781 -0
- package/dist/chunk-KWXPDASV.js.map +1 -0
- package/dist/chunk-O3PI7W45.js +68 -0
- package/dist/chunk-O3PI7W45.js.map +1 -0
- package/dist/chunk-QW7H3ICI.js +1372 -0
- package/dist/chunk-QW7H3ICI.js.map +1 -0
- package/dist/chunk-VPS7L64N.js +4785 -0
- package/dist/chunk-VPS7L64N.js.map +1 -0
- package/dist/chunk-Z4Y4FMQN.js +1097 -0
- package/dist/chunk-Z4Y4FMQN.js.map +1 -0
- package/dist/compat/nestjs/index.cjs +4883 -0
- package/dist/compat/nestjs/index.cjs.map +1 -0
- package/dist/compat/nestjs/index.d.cts +7 -0
- package/dist/compat/nestjs/index.d.ts +7 -0
- package/dist/compat/nestjs/index.js +84 -0
- package/dist/compat/nestjs/index.js.map +1 -0
- package/dist/core/index.cjs +1632 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +2 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +90 -0
- package/dist/core/index.js.map +1 -0
- package/dist/extra/index.cjs +6885 -0
- package/dist/extra/index.cjs.map +1 -0
- package/dist/extra/index.d.cts +5 -0
- package/dist/extra/index.d.ts +5 -0
- package/dist/extra/index.js +290 -0
- package/dist/extra/index.js.map +1 -0
- package/dist/graph/index.cjs +3225 -0
- package/dist/graph/index.cjs.map +1 -0
- package/dist/graph/index.d.cts +3 -0
- package/dist/graph/index.d.ts +3 -0
- package/dist/graph/index.js +25 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph-CL_ZDAj9.d.cts +605 -0
- package/dist/graph-D18qmsNm.d.ts +605 -0
- package/dist/index-B6SsZs2h.d.cts +3463 -0
- package/dist/index-B7eOdgEx.d.ts +449 -0
- package/dist/index-BHUvlQ3v.d.ts +3463 -0
- package/dist/index-BtK55IE2.d.ts +231 -0
- package/dist/index-BvhgZRHK.d.cts +231 -0
- package/dist/index-Bvy_6CaN.d.ts +452 -0
- package/dist/index-C3BMRmmp.d.cts +449 -0
- package/dist/index-C5mqLhMX.d.cts +452 -0
- package/dist/index-CP_QvbWu.d.ts +940 -0
- package/dist/index-D_geH2Bm.d.cts +940 -0
- package/dist/index.cjs +14843 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1517 -0
- package/dist/index.d.ts +1517 -0
- package/dist/index.js +3649 -0
- package/dist/index.js.map +1 -0
- package/dist/meta-BsF6Sag9.d.cts +607 -0
- package/dist/meta-BsF6Sag9.d.ts +607 -0
- package/dist/patterns/reactive-layout/index.cjs +4143 -0
- package/dist/patterns/reactive-layout/index.cjs.map +1 -0
- package/dist/patterns/reactive-layout/index.d.cts +3 -0
- package/dist/patterns/reactive-layout/index.d.ts +3 -0
- package/dist/patterns/reactive-layout/index.js +38 -0
- package/dist/patterns/reactive-layout/index.js.map +1 -0
- package/dist/reactive-log-BfvfNWQh.d.cts +137 -0
- package/dist/reactive-log-ohLmTXoZ.d.ts +137 -0
- package/package.json +256 -0
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Graph
|
|
3
|
+
} from "./chunk-6W5SGIGB.js";
|
|
4
|
+
import {
|
|
5
|
+
DATA,
|
|
6
|
+
INVALIDATE,
|
|
7
|
+
TEARDOWN,
|
|
8
|
+
__export,
|
|
9
|
+
derived,
|
|
10
|
+
emitWithBatch,
|
|
11
|
+
monotonicNs,
|
|
12
|
+
state
|
|
13
|
+
} from "./chunk-5X3LAO3B.js";
|
|
14
|
+
|
|
15
|
+
// src/patterns/reactive-layout/index.ts
|
|
16
|
+
var reactive_layout_exports = {};
|
|
17
|
+
__export(reactive_layout_exports, {
|
|
18
|
+
CanvasMeasureAdapter: () => CanvasMeasureAdapter,
|
|
19
|
+
CliMeasureAdapter: () => CliMeasureAdapter,
|
|
20
|
+
ImageSizeAdapter: () => ImageSizeAdapter,
|
|
21
|
+
NodeCanvasMeasureAdapter: () => NodeCanvasMeasureAdapter,
|
|
22
|
+
PrecomputedAdapter: () => PrecomputedAdapter,
|
|
23
|
+
SvgBoundsAdapter: () => SvgBoundsAdapter,
|
|
24
|
+
analyzeAndMeasure: () => analyzeAndMeasure,
|
|
25
|
+
computeBlockFlow: () => computeBlockFlow,
|
|
26
|
+
computeCharPositions: () => computeCharPositions,
|
|
27
|
+
computeLineBreaks: () => computeLineBreaks,
|
|
28
|
+
computeTotalHeight: () => computeTotalHeight,
|
|
29
|
+
measureBlock: () => measureBlock,
|
|
30
|
+
measureBlocks: () => measureBlocks,
|
|
31
|
+
reactiveBlockLayout: () => reactiveBlockLayout,
|
|
32
|
+
reactiveLayout: () => reactiveLayout
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// src/patterns/reactive-layout/measurement-adapters.ts
|
|
36
|
+
function cellWidth(code) {
|
|
37
|
+
if (code >= 768 && code <= 879 || // Combining Diacritical Marks
|
|
38
|
+
code >= 1155 && code <= 1161 || // Cyrillic combining marks
|
|
39
|
+
code >= 1425 && code <= 1469 || // Hebrew combining marks
|
|
40
|
+
code >= 1552 && code <= 1562 || // Arabic combining marks
|
|
41
|
+
code >= 1611 && code <= 1631 || // Arabic combining marks
|
|
42
|
+
code >= 1648 && code === 1648 || // Arabic superscript alef
|
|
43
|
+
code >= 1750 && code <= 1756 || // Arabic combining marks
|
|
44
|
+
code >= 1759 && code <= 1764 || // Arabic combining marks
|
|
45
|
+
code >= 1767 && code <= 1768 || // Arabic combining marks
|
|
46
|
+
code >= 1770 && code <= 1773 || // Arabic combining marks
|
|
47
|
+
code >= 1840 && code <= 1866 || // Syriac combining marks
|
|
48
|
+
code >= 1958 && code <= 1968 || // Thaana combining marks
|
|
49
|
+
code >= 2304 && code <= 2307 || // Devanagari combining marks
|
|
50
|
+
code >= 2362 && code <= 2383 || // Devanagari combining marks
|
|
51
|
+
code >= 2385 && code <= 2391 || // Devanagari combining marks
|
|
52
|
+
code >= 2402 && code <= 2403 || // Devanagari combining marks
|
|
53
|
+
code >= 2433 && code <= 2435 || // Bengali combining marks
|
|
54
|
+
code >= 2492 && code <= 2509 || // Bengali combining marks
|
|
55
|
+
code >= 2561 && code <= 2563 || // Gurmukhi combining marks
|
|
56
|
+
code >= 2620 && code <= 2641 || // Gurmukhi combining marks
|
|
57
|
+
code >= 2672 && code <= 2673 || // Gurmukhi combining marks
|
|
58
|
+
code >= 2677 && code === 2677 || // Gurmukhi combining mark
|
|
59
|
+
code >= 3633 && code === 3633 || // Thai combining mark
|
|
60
|
+
code >= 3636 && code <= 3642 || // Thai combining marks
|
|
61
|
+
code >= 3655 && code <= 3662 || // Thai combining marks
|
|
62
|
+
code >= 3761 && code === 3761 || // Lao combining mark
|
|
63
|
+
code >= 3764 && code <= 3772 || // Lao combining marks
|
|
64
|
+
code >= 3784 && code <= 3790 || // Lao combining marks
|
|
65
|
+
code >= 7616 && code <= 7679 || // Combining Diacritical Marks Supplement
|
|
66
|
+
code >= 8400 && code <= 8447 || // Combining Diacritical Marks for Symbols
|
|
67
|
+
code >= 65024 && code <= 65039 || // Variation Selectors
|
|
68
|
+
code >= 65056 && code <= 65071 || // Combining Half Marks
|
|
69
|
+
code === 8205) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (code >= 4352 && code <= 4447 || // Hangul Jamo
|
|
73
|
+
code >= 8986 && code <= 8987 || // Watch, Hourglass
|
|
74
|
+
code >= 9001 && code <= 9002 || // Angle brackets
|
|
75
|
+
code >= 9193 && code <= 9203 || // Media control symbols
|
|
76
|
+
code >= 9208 && code <= 9210 || // Media control symbols
|
|
77
|
+
code >= 9725 && code <= 9726 || // Medium squares
|
|
78
|
+
code >= 9748 && code <= 9749 || // Umbrella, Hot Beverage
|
|
79
|
+
code >= 9800 && code <= 9811 || // Zodiac symbols
|
|
80
|
+
code === 9855 || // Wheelchair
|
|
81
|
+
code === 9875 || // Anchor
|
|
82
|
+
code === 9889 || // High Voltage
|
|
83
|
+
code >= 9898 && code <= 9899 || // Medium circles
|
|
84
|
+
code >= 9917 && code <= 9918 || // Soccer, Baseball
|
|
85
|
+
code >= 9924 && code <= 9925 || // Snowman, Sun behind cloud
|
|
86
|
+
code === 9934 || // Ophiuchus
|
|
87
|
+
code === 9940 || // No Entry
|
|
88
|
+
code === 9962 || // Church
|
|
89
|
+
code >= 9970 && code <= 9971 || // Fountain, Golf
|
|
90
|
+
code === 9973 || // Sailboat
|
|
91
|
+
code === 9978 || // Tent
|
|
92
|
+
code === 9981 || // Fuel Pump
|
|
93
|
+
code === 9986 || // Scissors
|
|
94
|
+
code === 9989 || // Check Mark
|
|
95
|
+
code >= 9992 && code <= 9997 || // Airplane...Writing Hand
|
|
96
|
+
code === 9999 || // Pencil
|
|
97
|
+
code >= 10067 && code <= 10069 || // Question marks
|
|
98
|
+
code === 10071 || // Exclamation
|
|
99
|
+
code >= 10133 && code <= 10135 || // Plus, Minus, Divide
|
|
100
|
+
code === 10160 || // Curly Loop
|
|
101
|
+
code === 10175 || // Double Curly Loop
|
|
102
|
+
code >= 10548 && code <= 10549 || // Arrows
|
|
103
|
+
code >= 11013 && code <= 11015 || // Arrows
|
|
104
|
+
code >= 11035 && code <= 11036 || // Squares
|
|
105
|
+
code === 11088 || // Star
|
|
106
|
+
code === 11093 || // Circle
|
|
107
|
+
code >= 11904 && code <= 12350 || // CJK Radicals, Symbols, Punctuation
|
|
108
|
+
code >= 12352 && code <= 12447 || // Hiragana
|
|
109
|
+
code >= 12448 && code <= 12543 || // Katakana
|
|
110
|
+
code >= 12549 && code <= 12591 || // Bopomofo
|
|
111
|
+
code >= 12593 && code <= 12686 || // Hangul Compatibility Jamo
|
|
112
|
+
code >= 12688 && code <= 12771 || // Kanbun, CJK Strokes
|
|
113
|
+
code >= 12784 && code <= 12830 || // Katakana Phonetic Extensions
|
|
114
|
+
code >= 12832 && code <= 12871 || // Enclosed CJK
|
|
115
|
+
code >= 12880 && code <= 19903 || // CJK Extensions + Unified block
|
|
116
|
+
code >= 19968 && code <= 40959 || // CJK Unified Ideographs
|
|
117
|
+
code >= 43360 && code <= 43388 || // Hangul Jamo Extended-A
|
|
118
|
+
code >= 44032 && code <= 55203 || // Hangul Syllables
|
|
119
|
+
code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
|
|
120
|
+
code >= 65040 && code <= 65049 || // Vertical forms
|
|
121
|
+
code >= 65072 && code <= 65131 || // CJK Compatibility Forms
|
|
122
|
+
code >= 65281 && code <= 65376 || // Fullwidth Forms (excl. halfwidth)
|
|
123
|
+
code >= 65504 && code <= 65510 || // Fullwidth Signs
|
|
124
|
+
code >= 126980 && code === 126980 || // Mahjong Red Dragon
|
|
125
|
+
code === 127183 || // Joker
|
|
126
|
+
code >= 127344 && code <= 127345 || // A/B buttons
|
|
127
|
+
code === 127358 || // O button
|
|
128
|
+
code === 127359 || // P button
|
|
129
|
+
code === 127374 || // AB button
|
|
130
|
+
code >= 127377 && code <= 127386 || // Squared symbols
|
|
131
|
+
code >= 127456 && code <= 127487 || // Regional Indicator Symbols
|
|
132
|
+
code >= 127488 && code <= 127490 || // Enclosed ideographic
|
|
133
|
+
code === 127514 || // Squared CJK
|
|
134
|
+
code === 127535 || // Squared CJK
|
|
135
|
+
code >= 127538 && code <= 127546 || // Squared CJK
|
|
136
|
+
code >= 127568 && code <= 127569 || // Circled ideographic
|
|
137
|
+
code >= 127744 && code <= 129535 || // Misc Symbols / Emoticons / Emoji
|
|
138
|
+
code >= 129536 && code <= 129791 || // Chess, Symbols Extended-A
|
|
139
|
+
code >= 129792 && code <= 130047 || // Symbols for Legacy Computing
|
|
140
|
+
code >= 131072 && code <= 196605 || // CJK Extension B-F (excl. nonchars)
|
|
141
|
+
code >= 196608 && code <= 262141) {
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
function countCells(text) {
|
|
147
|
+
let cells = 0;
|
|
148
|
+
for (const ch of text) {
|
|
149
|
+
cells += cellWidth(ch.codePointAt(0));
|
|
150
|
+
}
|
|
151
|
+
return cells;
|
|
152
|
+
}
|
|
153
|
+
var CliMeasureAdapter = class {
|
|
154
|
+
cellPx;
|
|
155
|
+
constructor(opts) {
|
|
156
|
+
this.cellPx = opts?.cellPx ?? 8;
|
|
157
|
+
}
|
|
158
|
+
measureSegment(text, _font) {
|
|
159
|
+
return { width: countCells(text) * this.cellPx };
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var PrecomputedAdapterKeyError = class extends Error {
|
|
163
|
+
name = "KeyError";
|
|
164
|
+
};
|
|
165
|
+
var PrecomputedAdapter = class {
|
|
166
|
+
metrics;
|
|
167
|
+
fallback;
|
|
168
|
+
constructor(opts) {
|
|
169
|
+
this.metrics = opts.metrics;
|
|
170
|
+
const fb = opts.fallback ?? "per-char";
|
|
171
|
+
if (fb !== "per-char" && fb !== "error") {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`fallback must be 'per-char' or 'error', got ${JSON.stringify(opts.fallback)}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
this.fallback = fb;
|
|
177
|
+
}
|
|
178
|
+
measureSegment(text, font) {
|
|
179
|
+
const fontMap = this.metrics[font];
|
|
180
|
+
if (fontMap) {
|
|
181
|
+
const w = fontMap[text];
|
|
182
|
+
if (w !== void 0) return { width: w };
|
|
183
|
+
}
|
|
184
|
+
if (this.fallback === "error") {
|
|
185
|
+
throw new PrecomputedAdapterKeyError(
|
|
186
|
+
`PrecomputedAdapter: no metrics for segment ${JSON.stringify(text)} in font ${JSON.stringify(font)}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
let total = 0;
|
|
190
|
+
if (fontMap) {
|
|
191
|
+
for (const ch of text) {
|
|
192
|
+
const cw = fontMap[ch];
|
|
193
|
+
if (cw !== void 0) {
|
|
194
|
+
total += cw;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { width: total };
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var CanvasMeasureAdapter = class {
|
|
202
|
+
ctx = null;
|
|
203
|
+
currentFont = "";
|
|
204
|
+
emojiCorrection;
|
|
205
|
+
constructor(opts) {
|
|
206
|
+
this.emojiCorrection = opts?.emojiCorrection ?? 1;
|
|
207
|
+
}
|
|
208
|
+
getContext() {
|
|
209
|
+
if (!this.ctx) {
|
|
210
|
+
if (typeof OffscreenCanvas === "undefined") {
|
|
211
|
+
throw new Error(
|
|
212
|
+
"CanvasMeasureAdapter requires a browser environment with OffscreenCanvas support. Use CliMeasureAdapter or NodeCanvasMeasureAdapter for Node.js."
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const canvas = new OffscreenCanvas(0, 0);
|
|
216
|
+
const ctx = canvas.getContext("2d");
|
|
217
|
+
if (!ctx) throw new Error("CanvasMeasureAdapter: failed to get 2d context");
|
|
218
|
+
this.ctx = ctx;
|
|
219
|
+
}
|
|
220
|
+
return this.ctx;
|
|
221
|
+
}
|
|
222
|
+
measureSegment(text, font) {
|
|
223
|
+
const ctx = this.getContext();
|
|
224
|
+
if (font !== this.currentFont) {
|
|
225
|
+
ctx.font = font;
|
|
226
|
+
this.currentFont = font;
|
|
227
|
+
}
|
|
228
|
+
let width = ctx.measureText(text).width;
|
|
229
|
+
if (this.emojiCorrection !== 1 && /\p{Emoji_Presentation}/u.test(text)) {
|
|
230
|
+
width *= this.emojiCorrection;
|
|
231
|
+
}
|
|
232
|
+
return { width };
|
|
233
|
+
}
|
|
234
|
+
clearCache() {
|
|
235
|
+
this.currentFont = "";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var NodeCanvasMeasureAdapter = class {
|
|
239
|
+
ctx = null;
|
|
240
|
+
currentFont = "";
|
|
241
|
+
canvasModule;
|
|
242
|
+
constructor(canvasModule) {
|
|
243
|
+
this.canvasModule = canvasModule;
|
|
244
|
+
}
|
|
245
|
+
getContext() {
|
|
246
|
+
if (!this.ctx) {
|
|
247
|
+
const canvas = this.canvasModule.createCanvas(0, 0);
|
|
248
|
+
const ctx = canvas.getContext("2d");
|
|
249
|
+
if (!ctx) throw new Error("NodeCanvasMeasureAdapter: failed to get 2d context");
|
|
250
|
+
this.ctx = ctx;
|
|
251
|
+
}
|
|
252
|
+
return this.ctx;
|
|
253
|
+
}
|
|
254
|
+
measureSegment(text, font) {
|
|
255
|
+
const ctx = this.getContext();
|
|
256
|
+
if (font !== this.currentFont) {
|
|
257
|
+
ctx.font = font;
|
|
258
|
+
this.currentFont = font;
|
|
259
|
+
}
|
|
260
|
+
return { width: ctx.measureText(text).width };
|
|
261
|
+
}
|
|
262
|
+
clearCache() {
|
|
263
|
+
this.currentFont = "";
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var SvgBoundsAdapter = class {
|
|
267
|
+
measureSvg(content) {
|
|
268
|
+
const viewBoxMatch = content.match(/viewBox\s*=\s*["']([^"']+)["']/);
|
|
269
|
+
if (viewBoxMatch) {
|
|
270
|
+
const parts = viewBoxMatch[1].trim().split(/[\s,]+/);
|
|
271
|
+
if (parts.length >= 4) {
|
|
272
|
+
const w = Number.parseFloat(parts[2]);
|
|
273
|
+
const h = Number.parseFloat(parts[3]);
|
|
274
|
+
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
|
|
275
|
+
return { width: w, height: h };
|
|
276
|
+
}
|
|
277
|
+
throw new Error(
|
|
278
|
+
"SvgBoundsAdapter: viewBox width/height are missing, non-finite, or not positive"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const widthMatch = content.match(/<svg[^>]*\bwidth\s*=\s*["']?([\d.]+)/);
|
|
283
|
+
const heightMatch = content.match(/<svg[^>]*\bheight\s*=\s*["']?([\d.]+)/);
|
|
284
|
+
if (widthMatch && heightMatch) {
|
|
285
|
+
const w = Number.parseFloat(widthMatch[1]);
|
|
286
|
+
const h = Number.parseFloat(heightMatch[1]);
|
|
287
|
+
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
|
|
288
|
+
return { width: w, height: h };
|
|
289
|
+
}
|
|
290
|
+
throw new Error(
|
|
291
|
+
"SvgBoundsAdapter: svg width/height attributes are non-finite or not positive"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
throw new Error(
|
|
295
|
+
"SvgBoundsAdapter: cannot determine dimensions \u2014 SVG has no viewBox or width/height attributes"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
var ImageSizeAdapter = class {
|
|
300
|
+
sizes;
|
|
301
|
+
constructor(sizes) {
|
|
302
|
+
this.sizes = new Map(Object.entries(sizes));
|
|
303
|
+
}
|
|
304
|
+
measureImage(src) {
|
|
305
|
+
const dims = this.sizes.get(src);
|
|
306
|
+
if (!dims) {
|
|
307
|
+
throw new Error(`ImageSizeAdapter: no dimensions registered for ${JSON.stringify(src)}`);
|
|
308
|
+
}
|
|
309
|
+
return { width: dims.width, height: dims.height };
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/patterns/reactive-layout/reactive-layout.ts
|
|
314
|
+
function isCJK(s) {
|
|
315
|
+
for (const ch of s) {
|
|
316
|
+
const c = ch.codePointAt(0);
|
|
317
|
+
if (c >= 19968 && c <= 40959 || // CJK Unified Ideographs
|
|
318
|
+
c >= 13312 && c <= 19903 || // CJK Extension A
|
|
319
|
+
c >= 12288 && c <= 12351 || // CJK Symbols and Punctuation
|
|
320
|
+
c >= 12352 && c <= 12447 || // Hiragana
|
|
321
|
+
c >= 12448 && c <= 12543 || // Katakana
|
|
322
|
+
c >= 44032 && c <= 55215 || // Hangul
|
|
323
|
+
c >= 65280 && c <= 65519) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
var kinsokuStart = /* @__PURE__ */ new Set([
|
|
330
|
+
"\uFF0C",
|
|
331
|
+
"\uFF0E",
|
|
332
|
+
"\uFF01",
|
|
333
|
+
"\uFF1A",
|
|
334
|
+
"\uFF1B",
|
|
335
|
+
"\uFF1F",
|
|
336
|
+
"\u3001",
|
|
337
|
+
"\u3002",
|
|
338
|
+
"\u30FB",
|
|
339
|
+
"\uFF09",
|
|
340
|
+
"\u3015",
|
|
341
|
+
"\u3009",
|
|
342
|
+
"\u300B",
|
|
343
|
+
"\u300D",
|
|
344
|
+
"\u300F",
|
|
345
|
+
"\u3011"
|
|
346
|
+
]);
|
|
347
|
+
var leftStickyPunctuation = /* @__PURE__ */ new Set([
|
|
348
|
+
".",
|
|
349
|
+
",",
|
|
350
|
+
"!",
|
|
351
|
+
"?",
|
|
352
|
+
":",
|
|
353
|
+
";",
|
|
354
|
+
")",
|
|
355
|
+
"]",
|
|
356
|
+
"}",
|
|
357
|
+
"%",
|
|
358
|
+
'"',
|
|
359
|
+
"\u201D",
|
|
360
|
+
"\u2019",
|
|
361
|
+
"\xBB",
|
|
362
|
+
"\u203A",
|
|
363
|
+
"\u2026"
|
|
364
|
+
]);
|
|
365
|
+
function normalizeWhitespace(text) {
|
|
366
|
+
return text.replace(/[\t\n\r\f ]+/g, " ").replace(/^ | $/g, "");
|
|
367
|
+
}
|
|
368
|
+
function segmentText(normalized) {
|
|
369
|
+
const wordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
|
|
370
|
+
const pieces = [];
|
|
371
|
+
for (const s of wordSegmenter.segment(normalized)) {
|
|
372
|
+
const text = s.segment;
|
|
373
|
+
const isWordLike = s.isWordLike ?? false;
|
|
374
|
+
const texts = [];
|
|
375
|
+
const wordLikes = [];
|
|
376
|
+
const kinds = [];
|
|
377
|
+
let currentText = "";
|
|
378
|
+
let currentKind = null;
|
|
379
|
+
for (const ch of text) {
|
|
380
|
+
let kind;
|
|
381
|
+
if (ch === " ") kind = "space";
|
|
382
|
+
else if (ch === "\u200B") kind = "zero-width-break";
|
|
383
|
+
else if (ch === "\xAD") kind = "soft-hyphen";
|
|
384
|
+
else if (ch === "\n") kind = "hard-break";
|
|
385
|
+
else kind = "text";
|
|
386
|
+
if (currentKind !== null && kind === currentKind) {
|
|
387
|
+
currentText += ch;
|
|
388
|
+
} else {
|
|
389
|
+
if (currentKind !== null) {
|
|
390
|
+
texts.push(currentText);
|
|
391
|
+
wordLikes.push(currentKind === "text" && isWordLike);
|
|
392
|
+
kinds.push(currentKind);
|
|
393
|
+
}
|
|
394
|
+
currentText = ch;
|
|
395
|
+
currentKind = kind;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (currentKind !== null) {
|
|
399
|
+
texts.push(currentText);
|
|
400
|
+
wordLikes.push(currentKind === "text" && isWordLike);
|
|
401
|
+
kinds.push(currentKind);
|
|
402
|
+
}
|
|
403
|
+
pieces.push({ texts, isWordLike: wordLikes, kinds });
|
|
404
|
+
}
|
|
405
|
+
return pieces;
|
|
406
|
+
}
|
|
407
|
+
function analyzeAndMeasure(text, font, adapter, cache, stats) {
|
|
408
|
+
const normalized = normalizeWhitespace(text);
|
|
409
|
+
if (normalized.length === 0) return [];
|
|
410
|
+
const pieces = segmentText(normalized);
|
|
411
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, {
|
|
412
|
+
granularity: "grapheme"
|
|
413
|
+
});
|
|
414
|
+
const rawTexts = [];
|
|
415
|
+
const rawKinds = [];
|
|
416
|
+
const rawWordLike = [];
|
|
417
|
+
for (const piece of pieces) {
|
|
418
|
+
for (let i = 0; i < piece.texts.length; i++) {
|
|
419
|
+
rawTexts.push(piece.texts[i]);
|
|
420
|
+
rawKinds.push(piece.kinds[i]);
|
|
421
|
+
rawWordLike.push(piece.isWordLike[i]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const mergedTexts = [];
|
|
425
|
+
const mergedKinds = [];
|
|
426
|
+
const mergedWordLike = [];
|
|
427
|
+
for (let i = 0; i < rawTexts.length; i++) {
|
|
428
|
+
const t = rawTexts[i];
|
|
429
|
+
const k = rawKinds[i];
|
|
430
|
+
const wl = rawWordLike[i];
|
|
431
|
+
if (k === "text" && !wl && mergedTexts.length > 0 && mergedKinds[mergedKinds.length - 1] === "text") {
|
|
432
|
+
const isSticky = t.length === 1 && (leftStickyPunctuation.has(t) || kinsokuStart.has(t));
|
|
433
|
+
if (isSticky) {
|
|
434
|
+
mergedTexts[mergedTexts.length - 1] += t;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (t === "-" && mergedTexts.length > 0 && mergedKinds[mergedKinds.length - 1] === "text" && mergedWordLike[mergedWordLike.length - 1]) {
|
|
439
|
+
mergedTexts[mergedTexts.length - 1] += t;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
mergedTexts.push(t);
|
|
443
|
+
mergedKinds.push(k);
|
|
444
|
+
mergedWordLike.push(wl);
|
|
445
|
+
}
|
|
446
|
+
let fontCache = cache.get(font);
|
|
447
|
+
if (!fontCache) {
|
|
448
|
+
fontCache = /* @__PURE__ */ new Map();
|
|
449
|
+
cache.set(font, fontCache);
|
|
450
|
+
}
|
|
451
|
+
function measureCached(seg) {
|
|
452
|
+
let w = fontCache.get(seg);
|
|
453
|
+
if (w === void 0) {
|
|
454
|
+
if (stats) stats.misses += 1;
|
|
455
|
+
w = adapter.measureSegment(seg, font).width;
|
|
456
|
+
fontCache.set(seg, w);
|
|
457
|
+
} else if (stats) {
|
|
458
|
+
stats.hits += 1;
|
|
459
|
+
}
|
|
460
|
+
return w;
|
|
461
|
+
}
|
|
462
|
+
const segments = [];
|
|
463
|
+
for (let i = 0; i < mergedTexts.length; i++) {
|
|
464
|
+
const t = mergedTexts[i];
|
|
465
|
+
const k = mergedKinds[i];
|
|
466
|
+
if (k !== "text") {
|
|
467
|
+
segments.push({
|
|
468
|
+
text: t,
|
|
469
|
+
width: k === "space" ? measureCached(" ") * t.length : 0,
|
|
470
|
+
kind: k,
|
|
471
|
+
graphemeWidths: null
|
|
472
|
+
});
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (isCJK(t)) {
|
|
476
|
+
let unitText = "";
|
|
477
|
+
for (const gs of graphemeSegmenter.segment(t)) {
|
|
478
|
+
const grapheme = gs.segment;
|
|
479
|
+
if (unitText.length > 0 && kinsokuStart.has(grapheme)) {
|
|
480
|
+
unitText += grapheme;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (unitText.length > 0) {
|
|
484
|
+
const w2 = measureCached(unitText);
|
|
485
|
+
segments.push({
|
|
486
|
+
text: unitText,
|
|
487
|
+
width: w2,
|
|
488
|
+
kind: "text",
|
|
489
|
+
graphemeWidths: null
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
unitText = grapheme;
|
|
493
|
+
}
|
|
494
|
+
if (unitText.length > 0) {
|
|
495
|
+
const w2 = measureCached(unitText);
|
|
496
|
+
segments.push({
|
|
497
|
+
text: unitText,
|
|
498
|
+
width: w2,
|
|
499
|
+
kind: "text",
|
|
500
|
+
graphemeWidths: null
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const w = measureCached(t);
|
|
506
|
+
let graphemeWidths = null;
|
|
507
|
+
if (mergedWordLike[i] && t.length > 1) {
|
|
508
|
+
const gWidths = [];
|
|
509
|
+
for (const gs of graphemeSegmenter.segment(t)) {
|
|
510
|
+
gWidths.push(measureCached(gs.segment));
|
|
511
|
+
}
|
|
512
|
+
if (gWidths.length > 1) {
|
|
513
|
+
graphemeWidths = gWidths;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
segments.push({ text: t, width: w, kind: "text", graphemeWidths });
|
|
517
|
+
}
|
|
518
|
+
return segments;
|
|
519
|
+
}
|
|
520
|
+
function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
|
|
521
|
+
if (segments.length === 0) {
|
|
522
|
+
return { lines: [], lineCount: 0 };
|
|
523
|
+
}
|
|
524
|
+
const lines = [];
|
|
525
|
+
let lineW = 0;
|
|
526
|
+
let hasContent = false;
|
|
527
|
+
let lineStartSeg = 0;
|
|
528
|
+
let lineStartGrapheme = 0;
|
|
529
|
+
let lineEndSeg = 0;
|
|
530
|
+
let lineEndGrapheme = 0;
|
|
531
|
+
let pendingBreakSeg = -1;
|
|
532
|
+
let pendingBreakWidth = 0;
|
|
533
|
+
let fontCache = cache.get(font);
|
|
534
|
+
if (!fontCache) {
|
|
535
|
+
fontCache = /* @__PURE__ */ new Map();
|
|
536
|
+
cache.set(font, fontCache);
|
|
537
|
+
}
|
|
538
|
+
let hyphenWidth = fontCache.get("-");
|
|
539
|
+
if (hyphenWidth === void 0) {
|
|
540
|
+
hyphenWidth = adapter.measureSegment("-", font).width;
|
|
541
|
+
fontCache.set("-", hyphenWidth);
|
|
542
|
+
}
|
|
543
|
+
function emitLine(endSeg = lineEndSeg, endGrapheme = lineEndGrapheme, width = lineW) {
|
|
544
|
+
let text = "";
|
|
545
|
+
for (let i = lineStartSeg; i < endSeg; i++) {
|
|
546
|
+
const seg = segments[i];
|
|
547
|
+
if (seg.kind === "soft-hyphen" || seg.kind === "hard-break") continue;
|
|
548
|
+
if (i === lineStartSeg && lineStartGrapheme > 0 && seg.graphemeWidths) {
|
|
549
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, {
|
|
550
|
+
granularity: "grapheme"
|
|
551
|
+
});
|
|
552
|
+
const graphemes = [...graphemeSegmenter.segment(seg.text)].map((g) => g.segment);
|
|
553
|
+
text += graphemes.slice(lineStartGrapheme).join("");
|
|
554
|
+
} else {
|
|
555
|
+
text += seg.text;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (endGrapheme > 0 && endSeg < segments.length) {
|
|
559
|
+
const seg = segments[endSeg];
|
|
560
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, {
|
|
561
|
+
granularity: "grapheme"
|
|
562
|
+
});
|
|
563
|
+
const graphemes = [...graphemeSegmenter.segment(seg.text)].map((g) => g.segment);
|
|
564
|
+
const startG = lineStartSeg === endSeg ? lineStartGrapheme : 0;
|
|
565
|
+
text += graphemes.slice(startG, endGrapheme).join("");
|
|
566
|
+
}
|
|
567
|
+
if (endSeg > 0 && segments[endSeg - 1]?.kind === "soft-hyphen" && !(lineStartSeg === endSeg && lineStartGrapheme > 0)) {
|
|
568
|
+
text += "-";
|
|
569
|
+
}
|
|
570
|
+
lines.push({
|
|
571
|
+
text,
|
|
572
|
+
width,
|
|
573
|
+
startSegment: lineStartSeg,
|
|
574
|
+
startGrapheme: lineStartGrapheme,
|
|
575
|
+
endSegment: endSeg,
|
|
576
|
+
endGrapheme
|
|
577
|
+
});
|
|
578
|
+
lineW = 0;
|
|
579
|
+
hasContent = false;
|
|
580
|
+
pendingBreakSeg = -1;
|
|
581
|
+
pendingBreakWidth = 0;
|
|
582
|
+
}
|
|
583
|
+
function canBreakAfter(kind) {
|
|
584
|
+
return kind === "space" || kind === "zero-width-break" || kind === "soft-hyphen";
|
|
585
|
+
}
|
|
586
|
+
function startLine(segIdx, graphemeIdx, width) {
|
|
587
|
+
hasContent = true;
|
|
588
|
+
lineStartSeg = segIdx;
|
|
589
|
+
lineStartGrapheme = graphemeIdx;
|
|
590
|
+
lineEndSeg = segIdx + 1;
|
|
591
|
+
lineEndGrapheme = 0;
|
|
592
|
+
lineW = width;
|
|
593
|
+
}
|
|
594
|
+
function startLineAtGrapheme(segIdx, graphemeIdx, width) {
|
|
595
|
+
hasContent = true;
|
|
596
|
+
lineStartSeg = segIdx;
|
|
597
|
+
lineStartGrapheme = graphemeIdx;
|
|
598
|
+
lineEndSeg = segIdx;
|
|
599
|
+
lineEndGrapheme = graphemeIdx + 1;
|
|
600
|
+
lineW = width;
|
|
601
|
+
}
|
|
602
|
+
for (let i = 0; i < segments.length; i++) {
|
|
603
|
+
const seg = segments[i];
|
|
604
|
+
if (seg.kind === "hard-break") {
|
|
605
|
+
if (hasContent) {
|
|
606
|
+
emitLine();
|
|
607
|
+
} else {
|
|
608
|
+
lines.push({
|
|
609
|
+
text: "",
|
|
610
|
+
width: 0,
|
|
611
|
+
startSegment: i,
|
|
612
|
+
startGrapheme: 0,
|
|
613
|
+
endSegment: i,
|
|
614
|
+
endGrapheme: 0
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
lineStartSeg = i + 1;
|
|
618
|
+
lineStartGrapheme = 0;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const w = seg.width;
|
|
622
|
+
if (!hasContent) {
|
|
623
|
+
if (w > maxWidth && seg.graphemeWidths) {
|
|
624
|
+
appendBreakableSegment(i, 0, seg.graphemeWidths);
|
|
625
|
+
} else {
|
|
626
|
+
startLine(i, 0, w);
|
|
627
|
+
}
|
|
628
|
+
if (canBreakAfter(seg.kind)) {
|
|
629
|
+
pendingBreakSeg = i + 1;
|
|
630
|
+
pendingBreakWidth = seg.kind === "space" ? lineW - w : lineW;
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const newW = lineW + w;
|
|
635
|
+
if (newW > maxWidth + 5e-3) {
|
|
636
|
+
if (canBreakAfter(seg.kind)) {
|
|
637
|
+
lineW += w;
|
|
638
|
+
lineEndSeg = i + 1;
|
|
639
|
+
lineEndGrapheme = 0;
|
|
640
|
+
emitLine(i + 1, 0, seg.kind === "space" ? lineW - w : lineW);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (pendingBreakSeg >= 0) {
|
|
644
|
+
emitLine(pendingBreakSeg, 0, pendingBreakWidth);
|
|
645
|
+
i--;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (w > maxWidth && seg.graphemeWidths) {
|
|
649
|
+
emitLine();
|
|
650
|
+
appendBreakableSegment(i, 0, seg.graphemeWidths);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
emitLine();
|
|
654
|
+
i--;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
lineW = newW;
|
|
658
|
+
lineEndSeg = i + 1;
|
|
659
|
+
lineEndGrapheme = 0;
|
|
660
|
+
if (canBreakAfter(seg.kind)) {
|
|
661
|
+
pendingBreakSeg = i + 1;
|
|
662
|
+
pendingBreakWidth = seg.kind === "space" ? lineW - w : lineW;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (hasContent) {
|
|
666
|
+
emitLine();
|
|
667
|
+
}
|
|
668
|
+
return { lines, lineCount: lines.length };
|
|
669
|
+
function appendBreakableSegment(segIdx, startG, gWidths) {
|
|
670
|
+
for (let g = startG; g < gWidths.length; g++) {
|
|
671
|
+
const gw = gWidths[g];
|
|
672
|
+
if (!hasContent) {
|
|
673
|
+
startLineAtGrapheme(segIdx, g, gw);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (lineW + gw > maxWidth + 5e-3) {
|
|
677
|
+
emitLine();
|
|
678
|
+
startLineAtGrapheme(segIdx, g, gw);
|
|
679
|
+
} else {
|
|
680
|
+
lineW += gw;
|
|
681
|
+
lineEndSeg = segIdx;
|
|
682
|
+
lineEndGrapheme = g + 1;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (hasContent && lineEndSeg === segIdx && lineEndGrapheme === gWidths.length) {
|
|
686
|
+
lineEndSeg = segIdx + 1;
|
|
687
|
+
lineEndGrapheme = 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function computeCharPositions(lineBreaks, segments, lineHeight) {
|
|
692
|
+
const positions = [];
|
|
693
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, {
|
|
694
|
+
granularity: "grapheme"
|
|
695
|
+
});
|
|
696
|
+
for (let lineIdx = 0; lineIdx < lineBreaks.lines.length; lineIdx++) {
|
|
697
|
+
const line = lineBreaks.lines[lineIdx];
|
|
698
|
+
const y = lineIdx * lineHeight;
|
|
699
|
+
let x = 0;
|
|
700
|
+
for (let si = line.startSegment; si < segments.length; si++) {
|
|
701
|
+
const seg = segments[si];
|
|
702
|
+
if (seg.kind === "soft-hyphen" || seg.kind === "hard-break") {
|
|
703
|
+
if (si >= line.endSegment && line.endGrapheme === 0) break;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const graphemes = [...graphemeSegmenter.segment(seg.text)].map((g) => g.segment);
|
|
707
|
+
if (graphemes.length === 0) continue;
|
|
708
|
+
const startG = si === line.startSegment ? line.startGrapheme : 0;
|
|
709
|
+
let endG;
|
|
710
|
+
if (si < line.endSegment) {
|
|
711
|
+
endG = graphemes.length;
|
|
712
|
+
} else if (si === line.endSegment && line.endGrapheme > 0) {
|
|
713
|
+
endG = line.endGrapheme;
|
|
714
|
+
} else {
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
for (let g = startG; g < endG; g++) {
|
|
718
|
+
const gWidth = seg.graphemeWidths ? seg.graphemeWidths[g] : seg.width / graphemes.length;
|
|
719
|
+
positions.push({ x, y, width: gWidth, height: lineHeight, line: lineIdx });
|
|
720
|
+
x += gWidth;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return positions;
|
|
725
|
+
}
|
|
726
|
+
function reactiveLayout(opts) {
|
|
727
|
+
const { adapter, name = "reactive-layout" } = opts;
|
|
728
|
+
const g = new Graph(name);
|
|
729
|
+
const measureCache = /* @__PURE__ */ new Map();
|
|
730
|
+
const textNode = state(opts.text ?? "", { name: "text" });
|
|
731
|
+
const fontNode = state(opts.font ?? "16px sans-serif", {
|
|
732
|
+
name: "font"
|
|
733
|
+
});
|
|
734
|
+
const lineHeightNode = state(opts.lineHeight ?? 20, {
|
|
735
|
+
name: "line-height"
|
|
736
|
+
});
|
|
737
|
+
const maxWidthNode = state(Math.max(0, opts.maxWidth ?? 800), {
|
|
738
|
+
name: "max-width"
|
|
739
|
+
});
|
|
740
|
+
function graphemeWidthsEqual(a, b) {
|
|
741
|
+
if (a === null || b === null) return a === b;
|
|
742
|
+
if (a.length !== b.length) return false;
|
|
743
|
+
for (let i = 0; i < a.length; i++) {
|
|
744
|
+
if (a[i] !== b[i]) return false;
|
|
745
|
+
}
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
const segmentsNode = derived(
|
|
749
|
+
[textNode, fontNode],
|
|
750
|
+
([textVal, fontVal]) => {
|
|
751
|
+
const t0 = monotonicNs();
|
|
752
|
+
const measureStats = { hits: 0, misses: 0 };
|
|
753
|
+
const result = analyzeAndMeasure(
|
|
754
|
+
textVal,
|
|
755
|
+
fontVal,
|
|
756
|
+
adapter,
|
|
757
|
+
measureCache,
|
|
758
|
+
measureStats
|
|
759
|
+
);
|
|
760
|
+
const elapsed = monotonicNs() - t0;
|
|
761
|
+
const lookups = measureStats.hits + measureStats.misses;
|
|
762
|
+
const hitRate = lookups === 0 ? 1 : measureStats.hits / lookups;
|
|
763
|
+
const meta = segmentsNode.meta;
|
|
764
|
+
if (meta) {
|
|
765
|
+
const hr = hitRate;
|
|
766
|
+
const len = result.length;
|
|
767
|
+
const el = elapsed;
|
|
768
|
+
emitWithBatch((msgs) => meta["cache-hit-rate"]?.down(msgs), [[DATA, hr]], 3);
|
|
769
|
+
emitWithBatch((msgs) => meta["segment-count"]?.down(msgs), [[DATA, len]], 3);
|
|
770
|
+
emitWithBatch((msgs) => meta["layout-time-ns"]?.down(msgs), [[DATA, el]], 3);
|
|
771
|
+
}
|
|
772
|
+
return result;
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "segments",
|
|
776
|
+
meta: {
|
|
777
|
+
"cache-hit-rate": 0,
|
|
778
|
+
"segment-count": 0,
|
|
779
|
+
"layout-time-ns": 0
|
|
780
|
+
},
|
|
781
|
+
onMessage(msg) {
|
|
782
|
+
if (msg[0] === INVALIDATE || msg[0] === TEARDOWN) {
|
|
783
|
+
measureCache.clear();
|
|
784
|
+
adapter.clearCache?.();
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
},
|
|
788
|
+
equals: (a, b) => {
|
|
789
|
+
const sa = a;
|
|
790
|
+
const sb = b;
|
|
791
|
+
if (sa == null || sb == null) return sa === sb;
|
|
792
|
+
if (sa.length !== sb.length) return false;
|
|
793
|
+
for (let i = 0; i < sa.length; i++) {
|
|
794
|
+
const pa = sa[i];
|
|
795
|
+
const pb = sb[i];
|
|
796
|
+
if (pa.text !== pb.text || pa.width !== pb.width || pa.kind !== pb.kind || !graphemeWidthsEqual(pa.graphemeWidths ?? null, pb.graphemeWidths ?? null))
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
const lineBreaksNode = derived(
|
|
804
|
+
[segmentsNode, maxWidthNode, fontNode],
|
|
805
|
+
([segs, mw, font]) => {
|
|
806
|
+
return computeLineBreaks(
|
|
807
|
+
segs,
|
|
808
|
+
mw,
|
|
809
|
+
adapter,
|
|
810
|
+
font,
|
|
811
|
+
measureCache
|
|
812
|
+
);
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
name: "line-breaks",
|
|
816
|
+
equals: (a, b) => {
|
|
817
|
+
const la = a;
|
|
818
|
+
const lb = b;
|
|
819
|
+
if (la == null || lb == null) return la === lb;
|
|
820
|
+
if (la.lineCount !== lb.lineCount) return false;
|
|
821
|
+
for (let i = 0; i < la.lines.length; i++) {
|
|
822
|
+
const lineA = la.lines[i];
|
|
823
|
+
const lineB = lb.lines[i];
|
|
824
|
+
if (lineA.text !== lineB.text || lineA.width !== lineB.width || lineA.startSegment !== lineB.startSegment || lineA.startGrapheme !== lineB.startGrapheme || lineA.endSegment !== lineB.endSegment || lineA.endGrapheme !== lineB.endGrapheme)
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
const heightNode = derived(
|
|
832
|
+
[lineBreaksNode, lineHeightNode],
|
|
833
|
+
([lb, lh]) => lb.lineCount * lh,
|
|
834
|
+
{ name: "height" }
|
|
835
|
+
);
|
|
836
|
+
const charPositionsNode = derived(
|
|
837
|
+
[lineBreaksNode, segmentsNode, lineHeightNode],
|
|
838
|
+
([lb, segs, lh]) => {
|
|
839
|
+
return computeCharPositions(lb, segs, lh);
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: "char-positions",
|
|
843
|
+
equals: (a, b) => {
|
|
844
|
+
const ca = a;
|
|
845
|
+
const cb = b;
|
|
846
|
+
if (ca == null || cb == null) return ca === cb;
|
|
847
|
+
if (ca.length !== cb.length) return false;
|
|
848
|
+
for (let i = 0; i < ca.length; i++) {
|
|
849
|
+
if (ca[i].x !== cb[i].x || ca[i].y !== cb[i].y || ca[i].width !== cb[i].width)
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
g.add("text", textNode);
|
|
857
|
+
g.add("font", fontNode);
|
|
858
|
+
g.add("line-height", lineHeightNode);
|
|
859
|
+
g.add("max-width", maxWidthNode);
|
|
860
|
+
g.add("segments", segmentsNode);
|
|
861
|
+
g.add("line-breaks", lineBreaksNode);
|
|
862
|
+
g.add("height", heightNode);
|
|
863
|
+
g.add("char-positions", charPositionsNode);
|
|
864
|
+
g.connect("text", "segments");
|
|
865
|
+
g.connect("font", "segments");
|
|
866
|
+
g.connect("segments", "line-breaks");
|
|
867
|
+
g.connect("max-width", "line-breaks");
|
|
868
|
+
g.connect("font", "line-breaks");
|
|
869
|
+
g.connect("line-breaks", "height");
|
|
870
|
+
g.connect("line-height", "height");
|
|
871
|
+
g.connect("line-breaks", "char-positions");
|
|
872
|
+
g.connect("segments", "char-positions");
|
|
873
|
+
g.connect("line-height", "char-positions");
|
|
874
|
+
return {
|
|
875
|
+
graph: g,
|
|
876
|
+
setText: (text) => g.set("text", text),
|
|
877
|
+
setFont: (font) => g.set("font", font),
|
|
878
|
+
setLineHeight: (lh) => g.set("line-height", lh),
|
|
879
|
+
setMaxWidth: (mw) => g.set("max-width", Math.max(0, mw)),
|
|
880
|
+
segments: segmentsNode,
|
|
881
|
+
lineBreaks: lineBreaksNode,
|
|
882
|
+
height: heightNode,
|
|
883
|
+
charPositions: charPositionsNode
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/patterns/reactive-layout/reactive-block-layout.ts
|
|
888
|
+
function measureBlock(block, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight, index) {
|
|
889
|
+
switch (block.type) {
|
|
890
|
+
case "text": {
|
|
891
|
+
const font = block.font ?? defaultFont;
|
|
892
|
+
const lineHeight = block.lineHeight ?? defaultLineHeight;
|
|
893
|
+
const segments = analyzeAndMeasure(block.text, font, adapters.text, measureCache);
|
|
894
|
+
const lineBreaks = computeLineBreaks(segments, maxWidth, adapters.text, font, measureCache);
|
|
895
|
+
const charPositions = computeCharPositions(lineBreaks, segments, lineHeight);
|
|
896
|
+
const height = lineBreaks.lineCount * lineHeight;
|
|
897
|
+
let width = 0;
|
|
898
|
+
for (const line of lineBreaks.lines) {
|
|
899
|
+
if (line.width > width) width = line.width;
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
index,
|
|
903
|
+
type: "text",
|
|
904
|
+
width: Math.min(width, maxWidth),
|
|
905
|
+
height,
|
|
906
|
+
textSegments: segments,
|
|
907
|
+
textLineBreaks: lineBreaks,
|
|
908
|
+
textCharPositions: charPositions
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
case "image": {
|
|
912
|
+
let w;
|
|
913
|
+
let h;
|
|
914
|
+
if (block.naturalWidth != null && block.naturalHeight != null) {
|
|
915
|
+
w = block.naturalWidth;
|
|
916
|
+
h = block.naturalHeight;
|
|
917
|
+
} else if (adapters.image) {
|
|
918
|
+
const dims = adapters.image.measureImage(block.src);
|
|
919
|
+
w = dims.width;
|
|
920
|
+
h = dims.height;
|
|
921
|
+
} else {
|
|
922
|
+
throw new Error(
|
|
923
|
+
`Image block at index ${index} has no naturalWidth/naturalHeight and no ImageMeasurer adapter`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (w > maxWidth) {
|
|
927
|
+
h = h * maxWidth / w;
|
|
928
|
+
w = maxWidth;
|
|
929
|
+
}
|
|
930
|
+
return { index, type: "image", width: w, height: h };
|
|
931
|
+
}
|
|
932
|
+
case "svg": {
|
|
933
|
+
let w;
|
|
934
|
+
let h;
|
|
935
|
+
if (block.viewBox) {
|
|
936
|
+
w = block.viewBox.width;
|
|
937
|
+
h = block.viewBox.height;
|
|
938
|
+
} else if (adapters.svg) {
|
|
939
|
+
const dims = adapters.svg.measureSvg(block.content);
|
|
940
|
+
w = dims.width;
|
|
941
|
+
h = dims.height;
|
|
942
|
+
} else {
|
|
943
|
+
throw new Error(`SVG block at index ${index} has no viewBox and no SvgMeasurer adapter`);
|
|
944
|
+
}
|
|
945
|
+
if (w > maxWidth) {
|
|
946
|
+
h = h * maxWidth / w;
|
|
947
|
+
w = maxWidth;
|
|
948
|
+
}
|
|
949
|
+
return { index, type: "svg", width: w, height: h };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function measureBlocks(blocks, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight) {
|
|
954
|
+
return blocks.map(
|
|
955
|
+
(block, i) => measureBlock(block, maxWidth, adapters, measureCache, defaultFont, defaultLineHeight, i)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
function computeBlockFlow(measured, gap) {
|
|
959
|
+
const result = [];
|
|
960
|
+
let y = 0;
|
|
961
|
+
for (let i = 0; i < measured.length; i++) {
|
|
962
|
+
const m = measured[i];
|
|
963
|
+
result.push({ ...m, x: 0, y });
|
|
964
|
+
y += m.height + (i < measured.length - 1 ? gap : 0);
|
|
965
|
+
}
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
function computeTotalHeight(flow) {
|
|
969
|
+
if (flow.length === 0) return 0;
|
|
970
|
+
const last = flow[flow.length - 1];
|
|
971
|
+
return last.y + last.height;
|
|
972
|
+
}
|
|
973
|
+
function reactiveBlockLayout(opts) {
|
|
974
|
+
const {
|
|
975
|
+
adapters,
|
|
976
|
+
name = "reactive-block-layout",
|
|
977
|
+
defaultFont = "16px sans-serif",
|
|
978
|
+
defaultLineHeight = 20
|
|
979
|
+
} = opts;
|
|
980
|
+
const g = new Graph(name);
|
|
981
|
+
const measureCache = /* @__PURE__ */ new Map();
|
|
982
|
+
const blocksNode = state(opts.blocks ?? [], { name: "blocks" });
|
|
983
|
+
const maxWidthNode = state(Math.max(0, opts.maxWidth ?? 800), { name: "max-width" });
|
|
984
|
+
const gapNode = state(opts.gap ?? 0, { name: "gap" });
|
|
985
|
+
const measuredBlocksNode = derived(
|
|
986
|
+
[blocksNode, maxWidthNode],
|
|
987
|
+
([blocksVal, mwVal]) => {
|
|
988
|
+
const t0 = monotonicNs();
|
|
989
|
+
const result = measureBlocks(
|
|
990
|
+
blocksVal,
|
|
991
|
+
mwVal,
|
|
992
|
+
adapters,
|
|
993
|
+
measureCache,
|
|
994
|
+
defaultFont,
|
|
995
|
+
defaultLineHeight
|
|
996
|
+
);
|
|
997
|
+
const elapsed = monotonicNs() - t0;
|
|
998
|
+
const meta = measuredBlocksNode.meta;
|
|
999
|
+
if (meta) {
|
|
1000
|
+
emitWithBatch((msgs) => meta["block-count"]?.down(msgs), [[DATA, result.length]], 3);
|
|
1001
|
+
emitWithBatch((msgs) => meta["layout-time-ns"]?.down(msgs), [[DATA, elapsed]], 3);
|
|
1002
|
+
}
|
|
1003
|
+
return result;
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: "measured-blocks",
|
|
1007
|
+
meta: { "block-count": 0, "layout-time-ns": 0 },
|
|
1008
|
+
onMessage(msg, _depIndex, _actions) {
|
|
1009
|
+
if (msg[0] === INVALIDATE || msg[0] === TEARDOWN) {
|
|
1010
|
+
measureCache.clear();
|
|
1011
|
+
adapters.text.clearCache?.();
|
|
1012
|
+
}
|
|
1013
|
+
return false;
|
|
1014
|
+
},
|
|
1015
|
+
equals: (a, b) => {
|
|
1016
|
+
const ma = a;
|
|
1017
|
+
const mb = b;
|
|
1018
|
+
if (ma == null || mb == null) return ma === mb;
|
|
1019
|
+
if (ma.length !== mb.length) return false;
|
|
1020
|
+
for (let i = 0; i < ma.length; i++) {
|
|
1021
|
+
const ba = ma[i];
|
|
1022
|
+
const bb = mb[i];
|
|
1023
|
+
if (ba.type !== bb.type || ba.width !== bb.width || ba.height !== bb.height || ba.index !== bb.index)
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
const blockFlowNode = derived(
|
|
1031
|
+
[measuredBlocksNode, gapNode],
|
|
1032
|
+
([measured, gapVal]) => {
|
|
1033
|
+
return computeBlockFlow(measured, gapVal);
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
name: "block-flow",
|
|
1037
|
+
equals: (a, b) => {
|
|
1038
|
+
const fa = a;
|
|
1039
|
+
const fb = b;
|
|
1040
|
+
if (fa == null || fb == null) return fa === fb;
|
|
1041
|
+
if (fa.length !== fb.length) return false;
|
|
1042
|
+
for (let i = 0; i < fa.length; i++) {
|
|
1043
|
+
const pa = fa[i];
|
|
1044
|
+
const pb = fb[i];
|
|
1045
|
+
if (pa.x !== pb.x || pa.y !== pb.y || pa.width !== pb.width || pa.height !== pb.height)
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
const totalHeightNode = derived(
|
|
1053
|
+
[blockFlowNode],
|
|
1054
|
+
([flow]) => computeTotalHeight(flow),
|
|
1055
|
+
{ name: "total-height" }
|
|
1056
|
+
);
|
|
1057
|
+
g.add("blocks", blocksNode);
|
|
1058
|
+
g.add("max-width", maxWidthNode);
|
|
1059
|
+
g.add("gap", gapNode);
|
|
1060
|
+
g.add("measured-blocks", measuredBlocksNode);
|
|
1061
|
+
g.add("block-flow", blockFlowNode);
|
|
1062
|
+
g.add("total-height", totalHeightNode);
|
|
1063
|
+
g.connect("blocks", "measured-blocks");
|
|
1064
|
+
g.connect("max-width", "measured-blocks");
|
|
1065
|
+
g.connect("measured-blocks", "block-flow");
|
|
1066
|
+
g.connect("gap", "block-flow");
|
|
1067
|
+
g.connect("block-flow", "total-height");
|
|
1068
|
+
return {
|
|
1069
|
+
graph: g,
|
|
1070
|
+
setBlocks: (blocks) => g.set("blocks", blocks),
|
|
1071
|
+
setMaxWidth: (mw) => g.set("max-width", Math.max(0, mw)),
|
|
1072
|
+
setGap: (gap) => g.set("gap", gap),
|
|
1073
|
+
measuredBlocks: measuredBlocksNode,
|
|
1074
|
+
blockFlow: blockFlowNode,
|
|
1075
|
+
totalHeight: totalHeightNode
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export {
|
|
1080
|
+
CliMeasureAdapter,
|
|
1081
|
+
PrecomputedAdapter,
|
|
1082
|
+
CanvasMeasureAdapter,
|
|
1083
|
+
NodeCanvasMeasureAdapter,
|
|
1084
|
+
SvgBoundsAdapter,
|
|
1085
|
+
ImageSizeAdapter,
|
|
1086
|
+
analyzeAndMeasure,
|
|
1087
|
+
computeLineBreaks,
|
|
1088
|
+
computeCharPositions,
|
|
1089
|
+
reactiveLayout,
|
|
1090
|
+
measureBlock,
|
|
1091
|
+
measureBlocks,
|
|
1092
|
+
computeBlockFlow,
|
|
1093
|
+
computeTotalHeight,
|
|
1094
|
+
reactiveBlockLayout,
|
|
1095
|
+
reactive_layout_exports
|
|
1096
|
+
};
|
|
1097
|
+
//# sourceMappingURL=chunk-Z4Y4FMQN.js.map
|