@vyaz/core 0.0.8 → 0.0.10

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/dist/index.js CHANGED
@@ -1,4 +1,1643 @@
1
1
  // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = import.meta.require;
18
+
19
+ // src/measure/canvas-polyfill.ts
20
+ var exports_canvas_polyfill = {};
21
+ __export(exports_canvas_polyfill, {
22
+ registerCanvasFont: () => registerCanvasFont,
23
+ enableOfficeTextMeasure: () => enableOfficeTextMeasure,
24
+ disableOfficeTextMeasure: () => disableOfficeTextMeasure
25
+ });
26
+ async function _initNodeDeps() {
27
+ try {
28
+ const m = await import("module");
29
+ _require = m.createRequire(import.meta.url);
30
+ const canvas = _require("@napi-rs/canvas");
31
+ _createCanvas = canvas.createCanvas;
32
+ } catch {
33
+ _createCanvas = null;
34
+ }
35
+ }
36
+ function needsCanvasPolyfill() {
37
+ if (typeof globalThis.document === "undefined")
38
+ return true;
39
+ try {
40
+ const el = globalThis.document.createElement("canvas");
41
+ if (!el || typeof el.getContext !== "function")
42
+ return true;
43
+ return false;
44
+ } catch {
45
+ return true;
46
+ }
47
+ }
48
+ function parseFont(fontStr) {
49
+ const pxMatch = fontStr.match(/(\d+(?:\.\d+)?)px\s+(.+)/);
50
+ if (!pxMatch)
51
+ return null;
52
+ const size = parseFloat(pxMatch[1]);
53
+ const family = pxMatch[2].trim();
54
+ const weightMatch = fontStr.match(/\b(bold|italic|\d{3})\b/);
55
+ const weight = weightMatch ? weightMatch[1] === "bold" ? "bold" : weightMatch[1] : "normal";
56
+ return { family, size, weight };
57
+ }
58
+ function cacheKey(family, weight) {
59
+ return `${family}_${weight}_normal`;
60
+ }
61
+ function officeMeasureText(text) {
62
+ if (!officeFontCache) {
63
+ return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
64
+ }
65
+ const parsed = parseFont(this.font);
66
+ if (!parsed) {
67
+ return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
68
+ }
69
+ const key = cacheKey(parsed.family, parsed.weight);
70
+ const fontFace = officeFontCache.get(key);
71
+ if (!fontFace) {
72
+ return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
73
+ }
74
+ const raw = fontFace._raw;
75
+ const scale = parsed.size / fontFace.unitsPerEm;
76
+ let totalWidth = 0;
77
+ for (let i = 0;i < text.length; i++) {
78
+ const codePoint = text.codePointAt(i);
79
+ const glyph = raw.glyphForCodePoint(codePoint);
80
+ if (glyph) {
81
+ totalWidth += glyph.advanceWidth * scale;
82
+ } else {
83
+ if (originalMeasureText) {
84
+ return originalMeasureText.call(this, text);
85
+ }
86
+ totalWidth += parsed.size * 0.5;
87
+ }
88
+ if (codePoint > 65535)
89
+ i++;
90
+ }
91
+ return createMetricsObject(totalWidth);
92
+ }
93
+ function createMetricsObject(width) {
94
+ return {
95
+ width,
96
+ actualBoundingBoxAscent: 0,
97
+ actualBoundingBoxDescent: 0,
98
+ fontBoundingBoxAscent: 0,
99
+ fontBoundingBoxDescent: 0,
100
+ actualBoundingBoxLeft: 0,
101
+ actualBoundingBoxRight: width
102
+ };
103
+ }
104
+ function createEmptyMetrics() {
105
+ return createMetricsObject(0);
106
+ }
107
+ function registerCanvasFont(fontPath, family) {
108
+ if (!_require)
109
+ return;
110
+ try {
111
+ const mod = _require("@napi-rs/canvas");
112
+ if (mod?.registerFont) {
113
+ mod.registerFont(fontPath, { family });
114
+ }
115
+ } catch {}
116
+ }
117
+ function enableOfficeTextMeasure(fontCache) {
118
+ if (officeEnabled)
119
+ return;
120
+ officeFontCache = fontCache;
121
+ officeEnabled = true;
122
+ const CtxProto = globalThis.CanvasRenderingContext2D?.prototype;
123
+ if (CtxProto && originalMeasureText) {
124
+ CtxProto.measureText = officeMeasureText;
125
+ }
126
+ }
127
+ function disableOfficeTextMeasure() {
128
+ if (!officeEnabled)
129
+ return;
130
+ officeEnabled = false;
131
+ officeFontCache = null;
132
+ const CtxProto = globalThis.CanvasRenderingContext2D?.prototype;
133
+ if (CtxProto && originalMeasureText) {
134
+ CtxProto.measureText = originalMeasureText;
135
+ }
136
+ }
137
+ var _process, _require = null, _createCanvas = null, originalMeasureText, officeFontCache = null, officeEnabled = false;
138
+ var init_canvas_polyfill = __esm(async () => {
139
+ _process = typeof globalThis !== "undefined" ? globalThis.process : undefined;
140
+ if (_process && (_process.versions?.node || _process.versions?.bun)) {
141
+ await _initNodeDeps();
142
+ }
143
+ if (needsCanvasPolyfill()) {
144
+ globalThis.document = {
145
+ createElement: (tag) => {
146
+ if (tag === "canvas" && _createCanvas)
147
+ return _createCanvas(1, 1);
148
+ return {};
149
+ }
150
+ };
151
+ }
152
+ if (typeof globalThis.OffscreenCanvas === "undefined" && _createCanvas) {
153
+ globalThis.OffscreenCanvas = class OffscreenCanvasShim {
154
+ _canvas;
155
+ _w;
156
+ _h;
157
+ constructor(width, height) {
158
+ this._w = width;
159
+ this._h = height;
160
+ this._canvas = _createCanvas(width, height);
161
+ }
162
+ get width() {
163
+ return this._w;
164
+ }
165
+ set width(v) {
166
+ this._w = v;
167
+ this._canvas.width = v;
168
+ }
169
+ get height() {
170
+ return this._h;
171
+ }
172
+ set height(v) {
173
+ this._h = v;
174
+ this._canvas.height = v;
175
+ }
176
+ getContext(type, attrs) {
177
+ return this._canvas.getContext(type, attrs);
178
+ }
179
+ async convertToBlob({ type: _type } = {}) {
180
+ const buffer = this._canvas.toBuffer("image/png");
181
+ return new Blob([buffer], { type: "image/png" });
182
+ }
183
+ };
184
+ }
185
+ originalMeasureText = globalThis.CanvasRenderingContext2D?.prototype?.measureText;
186
+ });
187
+
188
+ // src/measure/FontEngine.ts
189
+ var exports_FontEngine = {};
190
+ __export(exports_FontEngine, {
191
+ isFontEngineAvailable: () => isFontEngineAvailable,
192
+ getGlyphAdvance: () => getGlyphAdvance,
193
+ createFontFace: () => createFontFace,
194
+ computePixelMetrics: () => computePixelMetrics
195
+ });
196
+ async function _getFontkit() {
197
+ if (_fontkitModule)
198
+ return _fontkitModule;
199
+ const mod = await import("fontkit");
200
+ _fontkitModule = mod.default || mod;
201
+ return _fontkitModule;
202
+ }
203
+ function _extractMetrics(raw) {
204
+ const os2 = raw["OS/2"];
205
+ return {
206
+ unitsPerEm: raw.unitsPerEm,
207
+ ascent: raw.ascent,
208
+ descent: raw.descent,
209
+ capHeight: raw.capHeight ?? raw.ascent,
210
+ winAscent: os2?.winAscent ?? null,
211
+ winDescent: os2?.winDescent ?? null
212
+ };
213
+ }
214
+ function _getGlyph(raw, codePoint) {
215
+ return raw.glyphForCodePoint(codePoint) ?? null;
216
+ }
217
+ async function createFontFace(buffer) {
218
+ const fontkit = await _getFontkit();
219
+ const raw = fontkit.create(buffer);
220
+ const metrics = _extractMetrics(raw);
221
+ return {
222
+ _raw: raw,
223
+ ...metrics
224
+ };
225
+ }
226
+ function getGlyphAdvance(font, codePoint) {
227
+ const glyph = _getGlyph(font._raw, codePoint);
228
+ if (!glyph)
229
+ return null;
230
+ return glyph.advanceWidth;
231
+ }
232
+ function computePixelMetrics(font, fontSize, mode) {
233
+ const scale = fontSize / font.unitsPerEm;
234
+ if (mode === "office" && font.winAscent != null && font.winDescent != null) {
235
+ return {
236
+ ascent: font.winAscent * scale * 1.078,
237
+ descent: Math.abs(font.winDescent) * scale * 1.078,
238
+ capHeight: (font.capHeight ?? font.ascent) * scale,
239
+ unitsPerEm: font.unitsPerEm,
240
+ sourceTable: "OS/2"
241
+ };
242
+ }
243
+ return {
244
+ ascent: font.ascent * scale,
245
+ descent: Math.abs(font.descent) * scale,
246
+ capHeight: (font.capHeight ?? font.ascent) * scale,
247
+ unitsPerEm: font.unitsPerEm,
248
+ sourceTable: "hhea"
249
+ };
250
+ }
251
+ async function isFontEngineAvailable() {
252
+ try {
253
+ const fk = await _getFontkit();
254
+ return typeof fk.create === "function";
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ var _fontkitModule = null;
260
+
261
+ // src/utils/font.ts
262
+ var exports_font = {};
263
+ __export(exports_font, {
264
+ getFontBuffer: () => getFontBuffer
265
+ });
266
+ async function getFontBuffer(fontUrl) {
267
+ const response = await fetch(fontUrl);
268
+ if (!response.ok) {
269
+ throw new Error(`[vyaz] Failed to download font from "${fontUrl}": ${response.status} ${response.statusText}`);
270
+ }
271
+ const buffer = await response.arrayBuffer();
272
+ return buffer;
273
+ }
274
+
275
+ // src/types/Document.ts
276
+ var DEFAULT_PARAGRAPH_STYLE = {
277
+ alignment: "left",
278
+ lineHeight: 1.15,
279
+ spaceBefore: 0,
280
+ spaceAfter: 0,
281
+ whiteSpace: "normal"
282
+ };
283
+ var DEFAULT_TEXT_STYLE = {
284
+ fontFamily: "Arial",
285
+ fontSize: 12,
286
+ fontWeight: "normal",
287
+ fontStyle: "normal",
288
+ color: "#000000"
289
+ };
290
+
291
+ // src/utils/textTransform.ts
292
+ function transformText(text, transform) {
293
+ if (!transform || transform === "none" || !text) {
294
+ return text;
295
+ }
296
+ switch (transform) {
297
+ case "uppercase":
298
+ return text.toUpperCase();
299
+ case "lowercase":
300
+ return text.toLowerCase();
301
+ case "capitalize": {
302
+ const segments = text.split(/([^\p{L}']+)/u);
303
+ const result = new Array(segments.length);
304
+ for (let i = 0;i < segments.length; i++) {
305
+ const seg = segments[i];
306
+ if (!seg) {
307
+ result[i] = seg;
308
+ continue;
309
+ }
310
+ if (/^[^\p{L}']+$/u.test(seg)) {
311
+ result[i] = seg;
312
+ continue;
313
+ }
314
+ const firstLetter = seg.match(/\p{L}/u);
315
+ if (!firstLetter) {
316
+ result[i] = seg;
317
+ continue;
318
+ }
319
+ const idx = firstLetter.index;
320
+ const before = seg.slice(0, idx);
321
+ const letter = seg[idx].toUpperCase();
322
+ const after = seg.slice(idx + 1);
323
+ result[i] = `${before}${letter}${after}`;
324
+ }
325
+ return result.join("");
326
+ }
327
+ default:
328
+ return text;
329
+ }
330
+ }
331
+
332
+ // src/compile/DocumentCompiler.ts
333
+ var FONT_WEIGHTS = {
334
+ thin: 100,
335
+ hairline: 100,
336
+ ultralight: 200,
337
+ extralight: 200,
338
+ light: 300,
339
+ normal: 400,
340
+ medium: 500,
341
+ semibold: 600,
342
+ demibold: 600,
343
+ bold: 700,
344
+ ultrabold: 800,
345
+ extrabold: 800,
346
+ heavy: 900,
347
+ black: 900
348
+ };
349
+ function normalizeFontWeight(weight) {
350
+ if (weight == null)
351
+ return FONT_WEIGHTS.normal;
352
+ if (typeof weight === "number")
353
+ return weight;
354
+ return FONT_WEIGHTS[weight.toLowerCase()] ?? FONT_WEIGHTS.normal;
355
+ }
356
+ function makeFontToken(run, effectiveFontSize) {
357
+ const fontStyle = run.fontStyle || DEFAULT_TEXT_STYLE.fontStyle || "normal";
358
+ const fontWeight = normalizeFontWeight(run.fontWeight ?? DEFAULT_TEXT_STYLE.fontWeight);
359
+ const fontFamily = run.fontFamily || DEFAULT_TEXT_STYLE.fontFamily || "Arial";
360
+ return `${fontStyle} ${fontWeight} ${effectiveFontSize}px ${fontFamily}`;
361
+ }
362
+ var SUPER_SUB_SCALE = 0.65;
363
+ var SUPER_OFFSET_RATIO = -0.4;
364
+ var SUB_OFFSET_RATIO = 0.25;
365
+ var COLLAPSIBLE_WS_RE = /[ \t\f\r]+/g;
366
+ function collapseSegmentWhitespace(segment) {
367
+ return segment.replace(COLLAPSIBLE_WS_RE, " ").trim();
368
+ }
369
+ function compileParagraph(paragraph) {
370
+ const items = [];
371
+ for (let i = 0;i < paragraph.children.length; i++) {
372
+ const run = paragraph.children[i];
373
+ const baseFontSize = run.fontSize ?? DEFAULT_TEXT_STYLE.fontSize ?? 12;
374
+ let effectiveFontSize = baseFontSize;
375
+ let baselineOffset = 0;
376
+ if (run.script === "super") {
377
+ effectiveFontSize = baseFontSize * SUPER_SUB_SCALE;
378
+ baselineOffset = baseFontSize * SUPER_OFFSET_RATIO;
379
+ } else if (run.script === "sub") {
380
+ effectiveFontSize = baseFontSize * SUPER_SUB_SCALE;
381
+ baselineOffset = baseFontSize * SUB_OFFSET_RATIO;
382
+ }
383
+ const rawText = run.type === "inline-box" ? "\uFFFC" : run.text;
384
+ const textTransformValue = run.textTransform;
385
+ const text = transformText(rawText, textTransformValue);
386
+ const resolvedFontWeight = normalizeFontWeight(run.fontWeight ?? DEFAULT_TEXT_STYLE.fontWeight);
387
+ const resolvedStyle = {
388
+ ...DEFAULT_TEXT_STYLE,
389
+ ...run,
390
+ fontSize: effectiveFontSize,
391
+ fontWeight: resolvedFontWeight,
392
+ text: run.text,
393
+ type: run.type
394
+ };
395
+ const item = {
396
+ text,
397
+ font: makeFontToken(run, effectiveFontSize),
398
+ letterSpacing: run.letterSpacing,
399
+ ...text !== rawText ? { originalText: rawText } : {},
400
+ metadata: {
401
+ originalRunIndex: i,
402
+ baselineOffset,
403
+ effectiveFontSize,
404
+ style: resolvedStyle,
405
+ inlineWidget: run.inlineWidget
406
+ }
407
+ };
408
+ if (run.type === "inline-box" && run.inlineWidget) {
409
+ item.extraWidth = run.inlineWidget.width;
410
+ item.break = "never";
411
+ }
412
+ items.push(item);
413
+ }
414
+ return items;
415
+ }
416
+ function splitParagraphByHardBreaks(paragraph) {
417
+ const ws = paragraph.style.whiteSpace;
418
+ if (ws !== "pre-line" && ws !== "pre" && ws !== "pre-wrap") {
419
+ return [paragraph];
420
+ }
421
+ const result = [];
422
+ let currentRuns = [];
423
+ for (const run of paragraph.children) {
424
+ const nlIndex = run.text.indexOf(`
425
+ `);
426
+ if (nlIndex === -1) {
427
+ currentRuns.push({ ...run });
428
+ continue;
429
+ }
430
+ const before = run.text.slice(0, nlIndex);
431
+ const after = run.text.slice(nlIndex + 1);
432
+ const textTransform = run.textTransform;
433
+ const effectiveFontSize = computeEffFs(run);
434
+ if (before.length > 0) {
435
+ const text = ws === "pre-line" ? collapseSegmentWhitespace(before) : before;
436
+ currentRuns.push({ ...run, text: transformText(text, textTransform) });
437
+ }
438
+ result.push({ style: paragraph.style, children: currentRuns });
439
+ let afterIdx = nlIndex + 1;
440
+ while (afterIdx < run.text.length && run.text[afterIdx] === `
441
+ `) {
442
+ result.push({ style: paragraph.style, children: [] });
443
+ afterIdx++;
444
+ }
445
+ const remaining = run.text.slice(afterIdx);
446
+ if (remaining.length > 0) {
447
+ const text = ws === "pre-line" ? collapseSegmentWhitespace(remaining) : remaining;
448
+ currentRuns = [{ ...run, text: transformText(text, textTransform) }];
449
+ } else {
450
+ currentRuns = [];
451
+ }
452
+ }
453
+ result.push({ style: paragraph.style, children: currentRuns });
454
+ return result;
455
+ }
456
+ function computeEffFs(run) {
457
+ const base = run.fontSize ?? DEFAULT_TEXT_STYLE.fontSize ?? 12;
458
+ return run.script === "super" || run.script === "sub" ? base * SUPER_SUB_SCALE : base;
459
+ }
460
+ function getParagraphText(paragraph) {
461
+ return paragraph.children.map((r) => r.text).join("");
462
+ }
463
+
464
+ // src/measure/FontMetricsProvider.ts
465
+ await init_canvas_polyfill();
466
+
467
+ // src/measure/FontNotFoundError.ts
468
+ class FontNotFoundError extends Error {
469
+ constructor(family, weight = "normal", style = "normal") {
470
+ super(`Font not found: "${family}" (weight: ${weight}, style: ${style}). ` + `Use SystemFontRegistry.scan() to register system fonts.`);
471
+ this.name = "FontNotFoundError";
472
+ }
473
+ }
474
+
475
+ // src/measure/FontMetricsProvider.ts
476
+ var MISSING_GLYPH_FACTOR = 0.5;
477
+ var WEIGHT_TO_NUM = {
478
+ thin: "100",
479
+ hairline: "100",
480
+ ultralight: "200",
481
+ extralight: "200",
482
+ light: "300",
483
+ normal: "400",
484
+ medium: "500",
485
+ semibold: "600",
486
+ demibold: "600",
487
+ bold: "700",
488
+ ultrabold: "800",
489
+ extrabold: "800",
490
+ heavy: "900",
491
+ black: "900"
492
+ };
493
+ function normaliseWeight(weight) {
494
+ const lower = weight.toLowerCase();
495
+ const mapped = WEIGHT_TO_NUM[lower];
496
+ if (mapped)
497
+ return mapped;
498
+ return weight;
499
+ }
500
+ function cacheKey2(family, weight, style) {
501
+ return `${family}_${normaliseWeight(weight)}_${style}`;
502
+ }
503
+
504
+ class FontMetricsProvider {
505
+ cache = new Map;
506
+ metricsCache = new Map;
507
+ mode = "browser";
508
+ pendingRegistrations = new Set;
509
+ _measureCanvas = null;
510
+ _measureCtx = null;
511
+ _getMeasureContext() {
512
+ if (!this._measureCtx) {
513
+ if (typeof OffscreenCanvas !== "undefined") {
514
+ this._measureCanvas = new OffscreenCanvas(1, 1);
515
+ this._measureCtx = this._measureCanvas.getContext("2d");
516
+ } else {
517
+ this._measureCanvas = document.createElement("canvas");
518
+ this._measureCtx = this._measureCanvas.getContext("2d");
519
+ }
520
+ }
521
+ return this._measureCtx;
522
+ }
523
+ setMode(mode) {
524
+ if (this.mode === mode)
525
+ return;
526
+ this.mode = mode;
527
+ this.metricsCache.clear();
528
+ if (mode === "office") {
529
+ enableOfficeTextMeasure(this.cache);
530
+ } else {
531
+ disableOfficeTextMeasure();
532
+ }
533
+ }
534
+ getMode() {
535
+ return this.mode;
536
+ }
537
+ async registerFont(family, options, source, sourcePath) {
538
+ const promise = this._registerFontInternal(family, options, source, sourcePath);
539
+ this.pendingRegistrations.add(promise);
540
+ try {
541
+ await promise;
542
+ } finally {
543
+ this.pendingRegistrations.delete(promise);
544
+ }
545
+ }
546
+ async _registerFontInternal(family, options, source, sourcePath) {
547
+ const { createFontFace: createFontFace2 } = await Promise.resolve().then(() => exports_FontEngine);
548
+ const { registerCanvasFont: registerCanvasFont2 } = await init_canvas_polyfill().then(() => exports_canvas_polyfill);
549
+ if (typeof source === "string") {
550
+ const { getFontBuffer: getFontBuffer2 } = await Promise.resolve().then(() => exports_font);
551
+ source = await getFontBuffer2(source);
552
+ }
553
+ const font = await createFontFace2(source);
554
+ const key = cacheKey2(family, options.weight || "normal", options.style || "normal");
555
+ this.cache.set(key, font);
556
+ this.metricsCache.delete(key);
557
+ if (sourcePath) {
558
+ registerCanvasFont2(sourcePath, family);
559
+ }
560
+ }
561
+ async waitForPendingRegistrations() {
562
+ await Promise.all(this.pendingRegistrations);
563
+ }
564
+ getFont(family, weight = "normal", style = "normal") {
565
+ const key = cacheKey2(family, weight, style);
566
+ return this.cache.get(key);
567
+ }
568
+ getMetrics(fontFamily, fontSize, weight = "normal", style = "normal") {
569
+ const _process2 = typeof globalThis !== "undefined" ? globalThis.process : undefined;
570
+ if (this.pendingRegistrations.size > 0 && _process2?.env?.NODE_ENV !== "production") {
571
+ console.warn("[vyaz] getMetrics() called while registerFont() promises are still pending. Wait for registerFont() to resolve before layout to avoid inaccurate metrics. Use fontMetricsProvider.waitForPendingRegistrations() if needed.");
572
+ }
573
+ const key = cacheKey2(fontFamily, weight, style);
574
+ const metricsKey = `${key}_${fontSize}_${this.mode}`;
575
+ const cached = this.metricsCache.get(metricsKey);
576
+ if (cached)
577
+ return cached;
578
+ const font = this.cache.get(key);
579
+ if (font) {
580
+ const scale = fontSize / font.unitsPerEm;
581
+ let ascent;
582
+ let descent;
583
+ let sourceTable;
584
+ if (this.mode === "office" && font.winAscent != null && font.winDescent != null) {
585
+ ascent = font.winAscent * scale * 1.078;
586
+ descent = Math.abs(font.winDescent) * scale * 1.078;
587
+ sourceTable = "OS/2";
588
+ } else {
589
+ ascent = font.ascent * scale;
590
+ descent = Math.abs(font.descent) * scale;
591
+ sourceTable = "hhea";
592
+ }
593
+ const metrics = {
594
+ ascent,
595
+ descent,
596
+ capHeight: (font.capHeight ?? font.ascent) * scale,
597
+ unitsPerEm: font.unitsPerEm,
598
+ sourceTable
599
+ };
600
+ this.metricsCache.set(metricsKey, metrics);
601
+ return metrics;
602
+ }
603
+ if (this.cache.size > 0) {
604
+ throw new FontNotFoundError(fontFamily, weight, style);
605
+ }
606
+ if (typeof document !== "undefined") {
607
+ try {
608
+ const ctx = this._getMeasureContext();
609
+ ctx.font = `${style} ${weight} ${fontSize}px ${fontFamily}`;
610
+ const m = ctx.measureText("M");
611
+ const metrics = {
612
+ ascent: m.fontBoundingBoxAscent || fontSize * 0.85,
613
+ descent: m.fontBoundingBoxDescent || fontSize * 0.15,
614
+ capHeight: m.actualBoundingBoxAscent || fontSize * 0.7,
615
+ unitsPerEm: 1000,
616
+ sourceTable: "canvas"
617
+ };
618
+ this.metricsCache.set(metricsKey, metrics);
619
+ return metrics;
620
+ } catch {}
621
+ }
622
+ throw new FontNotFoundError(fontFamily, weight, style);
623
+ }
624
+ }
625
+ var fontMetricsProvider = new FontMetricsProvider;
626
+
627
+ // src/layout/estimateWidth.ts
628
+ function correctToSumInvariant(measured, occupiedWidth) {
629
+ const sum = measured.reduce((a, b) => a + b, 0);
630
+ if (sum === 0 || Math.abs(occupiedWidth - sum) < 0.001) {
631
+ return measured;
632
+ }
633
+ const delta = occupiedWidth - sum;
634
+ return measured.map((m) => m + m / sum * delta);
635
+ }
636
+ function resolveFragmentWidths(fragments, fullText, occupiedWidth, measureFn) {
637
+ if (fragments.length === 0)
638
+ return [];
639
+ if (fragments.length === 1) {
640
+ return [occupiedWidth];
641
+ }
642
+ const measured = fragments.map((f) => measureFn(f));
643
+ return correctToSumInvariant(measured, occupiedWidth);
644
+ }
645
+
646
+ // src/utils/list.ts
647
+ var BULLET_CHARACTERS = {
648
+ 0: "\u2022",
649
+ 1: "\u25CB",
650
+ 2: "\u25AA"
651
+ };
652
+ var FALLBACK_BULLET = "\u25AA";
653
+ function defaultBulletChar(level) {
654
+ return BULLET_CHARACTERS[level] ?? FALLBACK_BULLET;
655
+ }
656
+ function formatListNumber(n, format) {
657
+ switch (format) {
658
+ case "decimal":
659
+ return String(n);
660
+ case "upper-roman":
661
+ return toRoman(n).toUpperCase();
662
+ case "lower-roman":
663
+ return toRoman(n).toLowerCase();
664
+ case "upper-alpha":
665
+ return toAlpha(n).toUpperCase();
666
+ case "lower-alpha":
667
+ return toAlpha(n).toLowerCase();
668
+ default:
669
+ return String(n);
670
+ }
671
+ }
672
+ function toRoman(n) {
673
+ if (n < 1 || n > 3999)
674
+ return String(n);
675
+ const romanMap = [
676
+ [1000, "m"],
677
+ [900, "cm"],
678
+ [500, "d"],
679
+ [400, "cd"],
680
+ [100, "c"],
681
+ [90, "xc"],
682
+ [50, "l"],
683
+ [40, "xl"],
684
+ [10, "x"],
685
+ [9, "ix"],
686
+ [5, "v"],
687
+ [4, "iv"],
688
+ [1, "i"]
689
+ ];
690
+ let result = "";
691
+ for (const [value, symbol] of romanMap) {
692
+ while (n >= value) {
693
+ result += symbol;
694
+ n -= value;
695
+ }
696
+ }
697
+ return result;
698
+ }
699
+ function toAlpha(n) {
700
+ if (n < 1)
701
+ return String(n);
702
+ const base = 26;
703
+ const offset = 97;
704
+ let result = "";
705
+ while (n > 0) {
706
+ n--;
707
+ result = String.fromCharCode(offset + n % base) + result;
708
+ n = Math.floor(n / base);
709
+ }
710
+ return result;
711
+ }
712
+
713
+ // src/layout/PositioningEngine.ts
714
+ function getMarkerText(listStyle, listIndex) {
715
+ if (listStyle.type === "bullet") {
716
+ return listStyle.bulletChar ?? defaultBulletChar(listStyle.level ?? 0);
717
+ }
718
+ if (listStyle.type === "number") {
719
+ const fmt = listStyle.numberFormat ?? "decimal";
720
+ return formatListNumber(listIndex, fmt) + ".";
721
+ }
722
+ return "";
723
+ }
724
+ function getParagraphFontSize(items) {
725
+ if (items.length === 0)
726
+ return 12;
727
+ return items[0].metadata.style.fontSize ?? 12;
728
+ }
729
+ function resolveBulletIndent(listStyle, paraFontSize) {
730
+ const level = listStyle.level ?? 0;
731
+ const indents = listStyle.indents;
732
+ if (indents && indents[level] !== undefined) {
733
+ return indents[level];
734
+ }
735
+ const defaultIndent = listStyle.bulletIndent ?? paraFontSize * 1.5;
736
+ return defaultIndent * (level + 1);
737
+ }
738
+ function positionLines(pretextLines, items, fontMetricsFn, style, maxWidth, startY = 0, mode = "browser", measureText, tag, listStyle, listIndex, listMarkerWidth) {
739
+ const lines = [];
740
+ let currentY = startY + style.spaceBefore;
741
+ let charIndex = 0;
742
+ let isFirstLine = true;
743
+ let contentWidth = 0;
744
+ const isListItem = listStyle && listStyle.type !== "none" && listIndex !== undefined;
745
+ let markerText = "";
746
+ let markerWidth = 0;
747
+ let bulletZoneIndent = 0;
748
+ let effectiveLeftIndent = style.leftIndent ?? 0;
749
+ if (isListItem) {
750
+ markerText = getMarkerText(listStyle, listIndex);
751
+ const paraFontSize = getParagraphFontSize(items);
752
+ bulletZoneIndent = resolveBulletIndent(listStyle, paraFontSize);
753
+ if (listMarkerWidth !== undefined && listMarkerWidth > bulletZoneIndent) {
754
+ bulletZoneIndent = listMarkerWidth;
755
+ }
756
+ const firstRunFontFamily = items[0]?.metadata.style.fontFamily ?? "Arial";
757
+ const firstRunFontWeight = String(items[0]?.metadata.style.fontWeight ?? 400);
758
+ const firstRunFontStyle = items[0]?.metadata.style.fontStyle ?? "normal";
759
+ const markerFontSize = listStyle.type === "bullet" ? paraFontSize * 0.9 : paraFontSize;
760
+ markerWidth = measureText(markerText, markerFontSize, firstRunFontFamily, firstRunFontWeight, firstRunFontStyle);
761
+ if (listStyle.position !== "inside") {
762
+ effectiveLeftIndent += bulletZoneIndent;
763
+ }
764
+ }
765
+ for (let lineIdx = 0;lineIdx < pretextLines.length; lineIdx++) {
766
+ const ptLine = pretextLines[lineIdx];
767
+ let maxAscent = 0;
768
+ let maxDescent = 0;
769
+ let maxLineHeightBase = 0;
770
+ const spans = [];
771
+ for (const frag of ptLine.fragments) {
772
+ const item = items[frag.itemIndex];
773
+ if (!item)
774
+ continue;
775
+ const metrics = fontMetricsFn(item);
776
+ const effectiveAscent = metrics.ascent - (item.metadata.baselineOffset || 0);
777
+ const effectiveDescent = metrics.descent + (item.metadata.baselineOffset || 0);
778
+ maxAscent = Math.max(maxAscent, effectiveAscent);
779
+ maxDescent = Math.max(maxDescent, effectiveDescent);
780
+ maxLineHeightBase = Math.max(maxLineHeightBase, metrics.ascent + metrics.descent);
781
+ const gapWidth = frag.gapBefore || 0;
782
+ const textWidth = frag.occupiedWidth;
783
+ const baseFontMetrics = {
784
+ ascent: metrics.ascent,
785
+ descent: metrics.descent,
786
+ fontSize: item.metadata.effectiveFontSize,
787
+ baselineOffset: item.metadata.baselineOffset || undefined
788
+ };
789
+ if (gapWidth > 0) {
790
+ spans.push({
791
+ x: 0,
792
+ width: gapWidth,
793
+ text: " ",
794
+ itemIndex: frag.itemIndex,
795
+ pIdx: 0,
796
+ tag,
797
+ fontMetrics: baseFontMetrics,
798
+ style: item.metadata.style,
799
+ inlineWidget: item.metadata.inlineWidget,
800
+ type: "space"
801
+ });
802
+ }
803
+ const text = frag.text;
804
+ const leadingMatch = text.match(/^(\s+)/);
805
+ let remainingText = text;
806
+ let leadingSpaceChars = 0;
807
+ let trailingSpaceChars = 0;
808
+ if (leadingMatch) {
809
+ leadingSpaceChars = leadingMatch[1].length;
810
+ remainingText = remainingText.slice(leadingSpaceChars);
811
+ }
812
+ if (remainingText.length > 0) {
813
+ const trailMatch = remainingText.match(/(\s+)$/);
814
+ if (trailMatch) {
815
+ trailingSpaceChars = trailMatch[1].length;
816
+ remainingText = remainingText.slice(0, -trailingSpaceChars);
817
+ }
818
+ }
819
+ const fragments = [];
820
+ if (leadingSpaceChars > 0)
821
+ fragments.push(text.slice(0, leadingSpaceChars));
822
+ if (remainingText.length > 0)
823
+ fragments.push(remainingText);
824
+ if (trailingSpaceChars > 0)
825
+ fragments.push(text.slice(leadingSpaceChars + remainingText.length));
826
+ const { fontFamily, fontWeight, fontStyle } = item.metadata.style;
827
+ const fsWeight = String(fontWeight || 400);
828
+ const fsStyle = fontStyle || "normal";
829
+ const fragmentMeasureFn = (t) => measureText(t, baseFontMetrics.fontSize, fontFamily, fsWeight, fsStyle);
830
+ const resolvedWidths = resolveFragmentWidths(fragments, text, textWidth, fragmentMeasureFn);
831
+ let resolvedIdx = 0;
832
+ const leadingWidth = leadingSpaceChars > 0 ? resolvedWidths[resolvedIdx++] : 0;
833
+ const trimmedWidth = remainingText.length > 0 ? resolvedWidths[resolvedIdx++] : 0;
834
+ const trailingWidthVal = trailingSpaceChars > 0 ? resolvedWidths[resolvedIdx++] : 0;
835
+ if (leadingSpaceChars > 0) {
836
+ const leadingText = text.slice(0, leadingSpaceChars);
837
+ spans.push({
838
+ x: 0,
839
+ width: leadingWidth,
840
+ text: leadingText,
841
+ itemIndex: frag.itemIndex,
842
+ pIdx: 0,
843
+ tag,
844
+ fontMetrics: baseFontMetrics,
845
+ style: item.metadata.style,
846
+ inlineWidget: item.metadata.inlineWidget,
847
+ type: "space"
848
+ });
849
+ }
850
+ if (remainingText.length > 0) {
851
+ const actualTextWidth = trimmedWidth;
852
+ spans.push({
853
+ x: 0,
854
+ width: actualTextWidth,
855
+ text: remainingText,
856
+ itemIndex: frag.itemIndex,
857
+ pIdx: 0,
858
+ tag,
859
+ fontMetrics: baseFontMetrics,
860
+ style: item.metadata.style,
861
+ inlineWidget: item.metadata.inlineWidget,
862
+ type: "text"
863
+ });
864
+ }
865
+ if (trailingSpaceChars > 0) {
866
+ const trailingStart = leadingSpaceChars + remainingText.length;
867
+ const trailingText = text.slice(trailingStart, trailingStart + trailingSpaceChars);
868
+ spans.push({
869
+ x: 0,
870
+ width: trailingWidthVal,
871
+ text: trailingText,
872
+ itemIndex: frag.itemIndex,
873
+ pIdx: 0,
874
+ tag,
875
+ fontMetrics: baseFontMetrics,
876
+ style: item.metadata.style,
877
+ inlineWidget: item.metadata.inlineWidget,
878
+ type: "space"
879
+ });
880
+ }
881
+ }
882
+ for (const s of spans) {
883
+ if (s.inlineWidget) {
884
+ s.width = s.inlineWidget.width;
885
+ }
886
+ }
887
+ let outsideMarkerSpan = null;
888
+ if (isListItem && isFirstLine) {
889
+ const paraFontSize = getParagraphFontSize(items);
890
+ const firstRun = items[0];
891
+ const markerFontSize = listStyle.type === "bullet" ? paraFontSize * 0.9 : paraFontSize;
892
+ const markerAscent = markerFontSize * 0.8;
893
+ const markerDescent = markerFontSize * 0.2;
894
+ maxAscent = Math.max(maxAscent, markerAscent);
895
+ maxDescent = Math.max(maxDescent, markerDescent);
896
+ maxLineHeightBase = Math.max(maxLineHeightBase, markerAscent + markerDescent);
897
+ const markerStyle = {
898
+ ...firstRun?.metadata.style ?? {
899
+ fontFamily: "Arial",
900
+ fontSize: markerFontSize,
901
+ fontWeight: 400,
902
+ fontStyle: "normal",
903
+ color: "#000000",
904
+ type: "text",
905
+ text: markerText
906
+ },
907
+ fontSize: markerFontSize,
908
+ text: markerText
909
+ };
910
+ const markerSpanBase = {
911
+ x: 0,
912
+ width: markerWidth,
913
+ text: markerText,
914
+ itemIndex: 0,
915
+ pIdx: 0,
916
+ tag,
917
+ fontMetrics: {
918
+ ascent: markerAscent,
919
+ descent: markerDescent,
920
+ fontSize: markerFontSize
921
+ },
922
+ style: markerStyle,
923
+ type: "marker"
924
+ };
925
+ if (listStyle.position === "inside") {
926
+ spans.unshift(markerSpanBase);
927
+ const gapWidth = markerFontSize * 0.5;
928
+ spans.splice(1, 0, {
929
+ x: 0,
930
+ width: gapWidth,
931
+ text: " ",
932
+ itemIndex: 0,
933
+ pIdx: 0,
934
+ tag,
935
+ fontMetrics: {
936
+ ascent: 0,
937
+ descent: 0,
938
+ fontSize: markerFontSize
939
+ },
940
+ style: markerStyle,
941
+ type: "space"
942
+ });
943
+ } else {
944
+ outsideMarkerSpan = markerSpanBase;
945
+ }
946
+ }
947
+ let trailingStartIndex = spans.length;
948
+ for (let i = spans.length - 1;i >= 0; i--) {
949
+ if (spans[i].type === "space") {
950
+ trailingStartIndex = i;
951
+ } else {
952
+ break;
953
+ }
954
+ }
955
+ const trailingWidth = spans.slice(trailingStartIndex).reduce((sum, f) => sum + f.width, 0);
956
+ for (let i = trailingStartIndex;i < spans.length; i++) {
957
+ spans[i].trailing = true;
958
+ }
959
+ const totalSpanWidth = spans.reduce((sum, f) => sum + f.width, 0);
960
+ const effectiveLineWidth = totalSpanWidth - trailingWidth;
961
+ const indent = isFirstLine ? effectiveLeftIndent + (style.indent || 0) : effectiveLeftIndent;
962
+ const rightIndent = style.rightIndent || 0;
963
+ const availableWidth = maxWidth - indent - rightIndent;
964
+ const slack = Math.max(0, availableWidth - effectiveLineWidth);
965
+ let xOffset = indent;
966
+ if (style.alignment === "center") {
967
+ xOffset = indent + slack / 2;
968
+ } else if (style.alignment === "right") {
969
+ xOffset = indent + slack;
970
+ } else if (style.alignment === "justify") {
971
+ const isLastLine = lineIdx === pretextLines.length - 1;
972
+ const stretchableSpaces = spans.filter((f) => f.type === "space" && !f.trailing);
973
+ const spaceCount = stretchableSpaces.length;
974
+ if (!isLastLine && spaceCount > 0 && slack > 0) {
975
+ const extraPerSpace = slack / spaceCount;
976
+ for (const sf of stretchableSpaces) {
977
+ sf.width += extraPerSpace;
978
+ }
979
+ }
980
+ xOffset = indent;
981
+ }
982
+ let runX = xOffset;
983
+ for (const frag of spans) {
984
+ frag.x = Math.round(runX * 100) / 100;
985
+ runX += frag.width;
986
+ }
987
+ if (outsideMarkerSpan) {
988
+ const gap = outsideMarkerSpan.fontMetrics.fontSize * 0.25;
989
+ outsideMarkerSpan.x = Math.round((xOffset - outsideMarkerSpan.width - gap) * 100) / 100;
990
+ spans.unshift(outsideMarkerSpan);
991
+ }
992
+ let lineX = xOffset;
993
+ let lineWidth = runX - xOffset;
994
+ if (spans.length > 0) {
995
+ const minSpanX = Math.min(...spans.map((s) => s.x));
996
+ const maxSpanRight = Math.max(...spans.map((s) => s.x + s.width));
997
+ lineX = minSpanX;
998
+ lineWidth = maxSpanRight - minSpanX;
999
+ }
1000
+ contentWidth = Math.max(contentWidth, lineX + lineWidth);
1001
+ const maxFontSize = spans.reduce((max, f) => Math.max(max, f.fontMetrics.fontSize), 0);
1002
+ const ascentRounded = Math.round(maxAscent);
1003
+ const descentRounded = Math.round(maxDescent);
1004
+ let lineBoxHeight;
1005
+ let baseline;
1006
+ if (mode === "office") {
1007
+ lineBoxHeight = maxLineHeightBase;
1008
+ baseline = ascentRounded;
1009
+ } else {
1010
+ const lineHeightPx = maxFontSize * style.lineHeight;
1011
+ const ascentDescentRounded = ascentRounded + descentRounded;
1012
+ const rawLineBoxHeight = Math.round(lineHeightPx);
1013
+ lineBoxHeight = Math.max(rawLineBoxHeight, ascentDescentRounded);
1014
+ const leading = lineBoxHeight - ascentDescentRounded;
1015
+ if (leading <= 0) {
1016
+ baseline = ascentRounded;
1017
+ } else {
1018
+ const ascentDescent = maxAscent + maxDescent;
1019
+ const aboveLeadingFloat = ascentDescent > 0 ? leading * maxAscent / ascentDescent : leading / 2;
1020
+ let aboveLeading = Math.round(aboveLeadingFloat);
1021
+ let belowLeading = leading - aboveLeading;
1022
+ if (aboveLeading >= belowLeading) {
1023
+ aboveLeading = Math.floor((leading - 1) / 2);
1024
+ belowLeading = leading - aboveLeading;
1025
+ }
1026
+ baseline = ascentRounded + aboveLeading;
1027
+ }
1028
+ }
1029
+ const startIdx = charIndex;
1030
+ let lineCharCount = 0;
1031
+ for (const frag of spans) {
1032
+ if (frag.type !== "marker") {
1033
+ lineCharCount += frag.text.length;
1034
+ }
1035
+ }
1036
+ const endIdx = startIdx + lineCharCount;
1037
+ charIndex = endIdx;
1038
+ if (spans.length > 0) {
1039
+ const lastSpan = spans[spans.length - 1];
1040
+ const lastSpanItem = items[lastSpan.itemIndex];
1041
+ if (lastSpanItem && lastSpan.text.endsWith(`
1042
+ `)) {
1043
+ lastSpan.breakType = "hard";
1044
+ } else if (lineIdx < pretextLines.length - 1) {
1045
+ lastSpan.breakType = "soft";
1046
+ }
1047
+ }
1048
+ lines.push({
1049
+ x: Math.round(lineX * 100) / 100,
1050
+ y: Math.round(currentY * 100) / 100,
1051
+ width: Math.round(lineWidth * 100) / 100,
1052
+ height: Math.round(lineBoxHeight * 100) / 100,
1053
+ baseline: Math.round(baseline * 100) / 100,
1054
+ ascent: Math.round(maxAscent * 100) / 100,
1055
+ descent: Math.round(maxDescent * 100) / 100,
1056
+ startIndex: startIdx,
1057
+ endIndex: endIdx,
1058
+ alignment: style.alignment,
1059
+ spans
1060
+ });
1061
+ currentY += lineBoxHeight;
1062
+ isFirstLine = false;
1063
+ }
1064
+ return { lines, contentWidth };
1065
+ }
1066
+
1067
+ // src/layout/LineBoxValidator.ts
1068
+ import { dump } from "js-yaml";
1069
+ var EPSILON = 0.5;
1070
+ function assertLineInvariants(lines, originalText, maxWidth) {
1071
+ if (lines.length === 0)
1072
+ return;
1073
+ const errors = [];
1074
+ for (let i = 0;i < lines.length; i++) {
1075
+ const line = lines[i];
1076
+ if (i > 0) {
1077
+ const prev = lines[i - 1];
1078
+ if (line.y < prev.y + prev.height - EPSILON) {
1079
+ errors.push({
1080
+ invariant: "NO_OVERLAP",
1081
+ message: `Line ${i} overlaps with line ${i - 1}`,
1082
+ details: {
1083
+ prevY: prev.y,
1084
+ prevHeight: prev.height,
1085
+ prevBottom: prev.y + prev.height,
1086
+ currentY: line.y
1087
+ }
1088
+ });
1089
+ }
1090
+ }
1091
+ if (i > 0 && line.y <= lines[i - 1].y) {
1092
+ errors.push({
1093
+ invariant: "MONOTONIC_Y",
1094
+ message: `Line ${i} has Y=${line.y} not > prev Y=${lines[i - 1].y}`
1095
+ });
1096
+ }
1097
+ if (line.endIndex <= line.startIndex) {
1098
+ errors.push({
1099
+ invariant: "INDEX_CONSIST",
1100
+ message: `Line ${i}: endIndex=${line.endIndex} <= startIndex=${line.startIndex}`
1101
+ });
1102
+ }
1103
+ if (maxWidth > 0 && line.width > maxWidth + EPSILON) {
1104
+ errors.push({
1105
+ invariant: "WIDTH_FIT",
1106
+ message: `Line ${i}: width=${line.width} > maxWidth=${maxWidth}`
1107
+ });
1108
+ }
1109
+ if (line.spans.length > 1) {
1110
+ const firstBaseline = line.baseline;
1111
+ for (let j = 0;j < line.spans.length; j++) {
1112
+ const span = line.spans[j];
1113
+ const spanBaseline = span.fontMetrics.ascent;
1114
+ if (Math.abs(spanBaseline - firstBaseline) > EPSILON) {
1115
+ if (spanBaseline <= 0) {
1116
+ errors.push({
1117
+ invariant: "BASELINE_EQ",
1118
+ message: `Line ${i}, span ${j}: baseline=${spanBaseline} is invalid`
1119
+ });
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ function spanStyleLabel(span) {
1127
+ if (span.style.fontStyle === "italic")
1128
+ return "italic";
1129
+ const w = span.style.fontWeight;
1130
+ if (w === "bold" || w === 700)
1131
+ return "bold";
1132
+ return "normal";
1133
+ }
1134
+ function linesToYAML(lines, paragraphWidth, paragraphHeight) {
1135
+ const obj = {
1136
+ width: paragraphWidth,
1137
+ height: paragraphHeight,
1138
+ lines: lines.map((line) => ({
1139
+ y: Math.round(line.y * 100) / 100,
1140
+ width: Math.round(line.width * 100) / 100,
1141
+ height: Math.round(line.height * 100) / 100,
1142
+ baseline: Math.round(line.baseline * 100) / 100,
1143
+ fragments: line.spans.map((span) => ({
1144
+ text: span.text,
1145
+ x: Math.round(span.x * 100) / 100,
1146
+ width: Math.round(span.width * 100) / 100,
1147
+ ...spanStyleLabel(span) !== "normal" ? { style: spanStyleLabel(span) } : {}
1148
+ }))
1149
+ }))
1150
+ };
1151
+ return dump(obj, {
1152
+ indent: 2,
1153
+ lineWidth: 120,
1154
+ noRefs: true,
1155
+ sortKeys: false
1156
+ });
1157
+ }
1158
+
1159
+ // src/layout/ParagraphLayoutEngine.ts
1160
+ import { prepareRichInline, materializeRichInlineLineRange, walkRichInlineLineRanges } from "@chenglou/pretext/rich-inline";
1161
+ function glyphCacheKey(text, fontSize, fontFamily, fontWeight, fontStyle) {
1162
+ return `${fontSize}_${fontFamily || ""}_${fontWeight || ""}_${fontStyle || ""}_${text}`;
1163
+ }
1164
+ function getFontMetricsForItem(item) {
1165
+ return fontMetricsProvider.getMetrics(item.metadata.style.fontFamily, item.metadata.effectiveFontSize, String(item.metadata.style.fontWeight || 400), item.metadata.style.fontStyle || "normal");
1166
+ }
1167
+
1168
+ class ParagraphLayoutEngine {
1169
+ preparedCache = new Map;
1170
+ layout(paragraph, maxWidth, yOffset = 0, fontProvider, listStyle, listIndex, listMarkerWidth) {
1171
+ const provider = fontProvider || fontMetricsProvider;
1172
+ const items = compileParagraph(paragraph);
1173
+ const cacheKey3 = JSON.stringify(paragraph);
1174
+ let prepared = this.preparedCache.get(cacheKey3);
1175
+ if (!prepared) {
1176
+ prepared = prepareRichInline(items);
1177
+ this.preparedCache.set(cacheKey3, prepared);
1178
+ }
1179
+ const effectiveMaxWidth = paragraph.style.whiteSpace === "nowrap" ? Infinity : maxWidth;
1180
+ const pretextLines = [];
1181
+ walkRichInlineLineRanges(prepared, effectiveMaxWidth, (range) => {
1182
+ pretextLines.push(range);
1183
+ });
1184
+ const materializedLines = [];
1185
+ for (const range of pretextLines) {
1186
+ materializedLines.push(materializeRichInlineLineRange(prepared, range));
1187
+ }
1188
+ const renderMode = provider.getMode();
1189
+ const glyphCache = new Map;
1190
+ const measureTextFn = (text, fontSize, fontFamily, fontWeight, fontStyle) => {
1191
+ if (!text)
1192
+ return 0;
1193
+ const key = glyphCacheKey(text, fontSize, fontFamily, fontWeight, fontStyle);
1194
+ let advances = glyphCache.get(key);
1195
+ if (!advances) {
1196
+ advances = this.computeGlyphAdvances(text, fontSize, fontFamily, fontWeight, fontStyle);
1197
+ glyphCache.set(key, advances);
1198
+ }
1199
+ let total = 0;
1200
+ for (let i = 0;i < advances.length; i++)
1201
+ total += advances[i];
1202
+ return Math.round(total * 100) / 100;
1203
+ };
1204
+ const { lines, contentWidth } = positionLines(materializedLines, items, (item) => {
1205
+ if (fontProvider) {
1206
+ return fontProvider.getMetrics(item.metadata.style.fontFamily, item.metadata.effectiveFontSize, String(item.metadata.style.fontWeight || 400), item.metadata.style.fontStyle || "normal");
1207
+ }
1208
+ return getFontMetricsForItem(item);
1209
+ }, paragraph.style, maxWidth, yOffset, renderMode, measureTextFn, paragraph.id, paragraph.style.listStyle, paragraph.style.listStyle ? listIndex ?? 1 : undefined, listMarkerWidth);
1210
+ for (const line of lines) {
1211
+ for (const span of line.spans) {
1212
+ if (span.type === "text" && span.text.length > 0 && !span.inlineWidget && !span.glyphAdvances) {
1213
+ const key = glyphCacheKey(span.text, span.fontMetrics.fontSize, span.style.fontFamily, String(span.style.fontWeight || 400), span.style.fontStyle || "normal");
1214
+ const cached = glyphCache.get(key);
1215
+ if (cached) {
1216
+ span.glyphAdvances = Array.from(cached);
1217
+ } else {
1218
+ span.glyphAdvances = Array.from(this.computeGlyphAdvances(span.text, span.fontMetrics.fontSize, span.style.fontFamily, String(span.style.fontWeight || 400), span.style.fontStyle || "normal"));
1219
+ }
1220
+ }
1221
+ }
1222
+ }
1223
+ assertLineInvariants(lines, getParagraphText(paragraph), maxWidth);
1224
+ const totalHeight = lines.length > 0 ? lines[lines.length - 1].y + lines[lines.length - 1].height + (paragraph.style.spaceAfter || 0) : 0;
1225
+ const contentHeight = lines.length > 0 ? lines[lines.length - 1].y + lines[lines.length - 1].height : 0;
1226
+ return { width: maxWidth, height: totalHeight, lines, contentWidth, contentHeight };
1227
+ }
1228
+ layoutGlyph(paragraph, maxWidth, yOffset = 0) {
1229
+ return this.layout(paragraph, maxWidth, yOffset);
1230
+ }
1231
+ computeGlyphAdvances(text, fontSize, fontFamily, fontWeight, fontStyle) {
1232
+ const font = fontMetricsProvider.getFont(fontFamily || "Arial", fontWeight || "400", fontStyle || "normal");
1233
+ if (!font) {
1234
+ throw new FontNotFoundError(fontFamily || "Arial", fontWeight || "400", fontStyle || "normal");
1235
+ }
1236
+ const scale = fontSize / font.unitsPerEm;
1237
+ const advances = new Float32Array(text.length);
1238
+ for (let i = 0;i < text.length; i++) {
1239
+ const codePoint = text.codePointAt(i);
1240
+ const advance = font._raw.glyphForCodePoint(codePoint)?.advanceWidth;
1241
+ if (advance != null) {
1242
+ advances[i] = advance * scale;
1243
+ } else {
1244
+ advances[i] = fontSize * MISSING_GLYPH_FACTOR;
1245
+ }
1246
+ if (codePoint > 65535)
1247
+ i++;
1248
+ }
1249
+ return advances;
1250
+ }
1251
+ }
1252
+ var paragraphLayoutEngine = new ParagraphLayoutEngine;
1253
+ // src/layout/TextFrameLayoutEngine.ts
1254
+ function getMarkerTextHelper(listStyle, listIndex) {
1255
+ if (listStyle.type === "bullet") {
1256
+ return listStyle.bulletChar ?? defaultBulletChar(listStyle.level ?? 0);
1257
+ }
1258
+ if (listStyle.type === "number") {
1259
+ const fmt = listStyle.numberFormat ?? "decimal";
1260
+ return formatListNumber(listIndex, fmt) + ".";
1261
+ }
1262
+ return "";
1263
+ }
1264
+ function computeMaxMarkerWidth(listStyle, startIndex, count, measureText) {
1265
+ if (listStyle.type !== "number")
1266
+ return 0;
1267
+ let maxWidth = 0;
1268
+ for (let i = 0;i < count; i++) {
1269
+ const markerText = getMarkerTextHelper(listStyle, startIndex + i);
1270
+ const width = measureText(markerText, 12);
1271
+ maxWidth = Math.max(maxWidth, width);
1272
+ }
1273
+ return maxWidth;
1274
+ }
1275
+ function applyVerticalAlignment(lines, colHeight, alignment) {
1276
+ if (alignment === "top" || lines.length === 0)
1277
+ return;
1278
+ const firstLineY = lines[0].y;
1279
+ const lastLineEnd = lines[lines.length - 1].y + lines[lines.length - 1].height;
1280
+ const contentHeight = lastLineEnd - firstLineY;
1281
+ const extraSpace = colHeight - contentHeight;
1282
+ if (extraSpace <= 0)
1283
+ return;
1284
+ let offset = 0;
1285
+ if (alignment === "middle") {
1286
+ offset = extraSpace / 2;
1287
+ } else if (alignment === "bottom") {
1288
+ offset = extraSpace;
1289
+ }
1290
+ for (const line of lines) {
1291
+ line.y += offset;
1292
+ }
1293
+ }
1294
+ function layoutTextFrame(frame) {
1295
+ const leftPad = frame.padding?.left ?? 0;
1296
+ const rightPad = frame.padding?.right ?? 0;
1297
+ const topPad = frame.padding?.top ?? 0;
1298
+ const bottomPad = frame.padding?.bottom ?? 0;
1299
+ const hasColumns = frame.columns != null && frame.columns.count > 1 && frame.width != null;
1300
+ let colWidth;
1301
+ let colCount = 1;
1302
+ let colGap = 0;
1303
+ if (hasColumns) {
1304
+ colCount = frame.columns.count;
1305
+ colGap = frame.columns.gap;
1306
+ const totalPad = leftPad + rightPad + (colCount - 1) * colGap;
1307
+ colWidth = (frame.width - totalPad) / colCount;
1308
+ }
1309
+ const colHeight = frame.height != null ? frame.height - topPad - bottomPad : Infinity;
1310
+ const verticalAlign = frame.verticalAlignment ?? "top";
1311
+ const listIndices = new Array(frame.paragraphs.length).fill(undefined);
1312
+ const listMarkerWidths = new Array(frame.paragraphs.length).fill(undefined);
1313
+ let i = 0;
1314
+ while (i < frame.paragraphs.length) {
1315
+ const p = frame.paragraphs[i];
1316
+ const ls = p.style.listStyle;
1317
+ if (!ls || ls.type === "none") {
1318
+ i++;
1319
+ continue;
1320
+ }
1321
+ let groupStart = i;
1322
+ let groupEnd = i + 1;
1323
+ while (groupEnd < frame.paragraphs.length) {
1324
+ const nextP = frame.paragraphs[groupEnd];
1325
+ const nextLs = nextP.style.listStyle;
1326
+ if (!nextLs || nextLs.type !== ls.type || nextP.style.listRestart) {
1327
+ break;
1328
+ }
1329
+ if ((nextLs.level ?? 0) !== (ls.level ?? 0)) {
1330
+ break;
1331
+ }
1332
+ groupEnd++;
1333
+ }
1334
+ const groupSize = groupEnd - groupStart;
1335
+ const startNumber = ls.startNumber ?? 1;
1336
+ for (let j = 0;j < groupSize; j++) {
1337
+ listIndices[groupStart + j] = startNumber + j;
1338
+ }
1339
+ const paraFontSize = p.children[0]?.fontSize ?? 12;
1340
+ const measureMarkerWidth = (text, fontSize) => {
1341
+ return text.length * fontSize * 0.6;
1342
+ };
1343
+ const maxMW = computeMaxMarkerWidth(ls, startNumber, groupSize, measureMarkerWidth);
1344
+ for (let j = 0;j < groupSize; j++) {
1345
+ listMarkerWidths[groupStart + j] = maxMW;
1346
+ }
1347
+ i = groupEnd;
1348
+ }
1349
+ const allLines = [];
1350
+ let contentWidth = 0;
1351
+ const currentColY = new Array(colCount).fill(topPad);
1352
+ function layoutSingleParagraph(p, maxWidth, pIdx, listIndex, listMarkerWidth) {
1353
+ const listStyle = p.style.listStyle;
1354
+ if (p.children.length === 0) {
1355
+ const fontSize = 12;
1356
+ const lineHeight = p.style.lineHeight;
1357
+ const lineHeightPx = Math.round(fontSize * lineHeight);
1358
+ return {
1359
+ lines: [{
1360
+ x: 0,
1361
+ y: 0,
1362
+ width: 0,
1363
+ height: lineHeightPx,
1364
+ baseline: Math.round(fontSize * 0.8),
1365
+ ascent: Math.round(fontSize * 0.8),
1366
+ descent: Math.round(fontSize * 0.2),
1367
+ startIndex: 0,
1368
+ endIndex: 0,
1369
+ isHardBreak: true,
1370
+ spans: []
1371
+ }],
1372
+ height: lineHeightPx,
1373
+ contentWidth: 0
1374
+ };
1375
+ }
1376
+ const result = paragraphLayoutEngine.layout(p, maxWidth, 0, undefined, listStyle, listIndex, listMarkerWidth);
1377
+ return {
1378
+ lines: result.lines,
1379
+ height: result.contentHeight,
1380
+ contentWidth: result.contentWidth
1381
+ };
1382
+ }
1383
+ for (let i2 = 0;i2 < frame.paragraphs.length; i2++) {
1384
+ const p = frame.paragraphs[i2];
1385
+ const subParagraphs = splitParagraphByHardBreaks(p);
1386
+ const maxWidth = hasColumns ? colWidth : frame.width !== undefined ? frame.width - leftPad - rightPad : Infinity;
1387
+ if (frame.wrap === false) {
1388
+ p.style = { ...p.style, whiteSpace: "nowrap" };
1389
+ }
1390
+ const listIndex = listIndices[i2];
1391
+ const listMarkerWidth = listMarkerWidths[i2];
1392
+ for (let subIdx = 0;subIdx < subParagraphs.length; subIdx++) {
1393
+ const subPara = subParagraphs[subIdx];
1394
+ if (!hasColumns && subIdx === 0) {
1395
+ currentColY[0] += p.style.spaceBefore;
1396
+ }
1397
+ const subResult = layoutSingleParagraph(subPara, maxWidth, i2, listIndex, listMarkerWidth);
1398
+ for (const line of subResult.lines) {
1399
+ if (!hasColumns) {
1400
+ line.x += leftPad;
1401
+ for (const span of line.spans) {
1402
+ span.pIdx = i2;
1403
+ }
1404
+ allLines.push(line);
1405
+ contentWidth = Math.max(contentWidth, subResult.contentWidth);
1406
+ line.y = currentColY[0];
1407
+ currentColY[0] += line.height;
1408
+ continue;
1409
+ }
1410
+ let colIdx = 0;
1411
+ for (let c = 0;c < colCount; c++) {
1412
+ if (currentColY[c] < currentColY[colIdx])
1413
+ colIdx = c;
1414
+ }
1415
+ let placed = false;
1416
+ for (let attempt = 0;attempt < colCount; attempt++) {
1417
+ if (currentColY[colIdx] + line.height <= colHeight) {
1418
+ line.x = colIdx * (colWidth + colGap) + leftPad;
1419
+ line.y = currentColY[colIdx];
1420
+ line.columnIndex = colIdx;
1421
+ currentColY[colIdx] += line.height;
1422
+ for (const span of line.spans) {
1423
+ span.pIdx = i2;
1424
+ }
1425
+ allLines.push(line);
1426
+ contentWidth = Math.max(contentWidth, line.x + line.width + rightPad);
1427
+ placed = true;
1428
+ break;
1429
+ }
1430
+ colIdx = (colIdx + 1) % colCount;
1431
+ if (attempt === colCount - 1) {
1432
+ line.x = colIdx * (colWidth + colGap) + leftPad;
1433
+ line.y = currentColY[colIdx];
1434
+ line.columnIndex = colIdx;
1435
+ currentColY[colIdx] += line.height;
1436
+ for (const span of line.spans) {
1437
+ span.pIdx = i2;
1438
+ }
1439
+ allLines.push(line);
1440
+ contentWidth = Math.max(contentWidth, line.x + line.width + rightPad);
1441
+ placed = true;
1442
+ }
1443
+ }
1444
+ if (!placed) {
1445
+ line.x = leftPad;
1446
+ line.y = currentColY[0];
1447
+ line.columnIndex = 0;
1448
+ currentColY[0] += line.height;
1449
+ for (const span of line.spans) {
1450
+ span.pIdx = i2;
1451
+ }
1452
+ allLines.push(line);
1453
+ }
1454
+ }
1455
+ if (!hasColumns && subPara === subParagraphs[subParagraphs.length - 1]) {
1456
+ currentColY[0] += p.style.spaceAfter;
1457
+ }
1458
+ }
1459
+ }
1460
+ if (hasColumns && verticalAlign !== "top" && frame.height != null) {
1461
+ const colLines = new Array(colCount).fill(null).map(() => []);
1462
+ for (const line of allLines) {
1463
+ const ci = line.columnIndex ?? 0;
1464
+ colLines[ci].push(line);
1465
+ }
1466
+ for (let c = 0;c < colCount; c++) {
1467
+ applyVerticalAlignment(colLines[c], colHeight, verticalAlign);
1468
+ }
1469
+ }
1470
+ if (hasColumns) {
1471
+ contentWidth = frame.width;
1472
+ } else {
1473
+ contentWidth += rightPad;
1474
+ }
1475
+ const lastLine = allLines.length > 0 ? allLines[allLines.length - 1] : null;
1476
+ const contentHeight = lastLine ? lastLine.y + lastLine.height + bottomPad : bottomPad;
1477
+ return {
1478
+ lines: allLines,
1479
+ frameWidth: frame.width,
1480
+ frameHeight: frame.height,
1481
+ contentWidth,
1482
+ contentHeight,
1483
+ fitHorizontal: frame.width !== undefined ? "frame" : "content",
1484
+ fitVertical: frame.height !== undefined ? "frame" : "content"
1485
+ };
1486
+ }
1487
+ // src/layout/AutoFitEngine.ts
1488
+ function applyScale(doc, scale) {
1489
+ const clone = JSON.parse(JSON.stringify(doc));
1490
+ for (const paragraph of clone.paragraphs) {
1491
+ for (const run of paragraph.children) {
1492
+ run.fontSize = Math.round(run.fontSize * scale * 100) / 100;
1493
+ }
1494
+ }
1495
+ if (clone.defaultStyle?.fontSize) {
1496
+ clone.defaultStyle.fontSize = Math.round(clone.defaultStyle.fontSize * scale * 100) / 100;
1497
+ }
1498
+ return clone;
1499
+ }
1500
+ function findScale(doc, layoutFn, config, options) {
1501
+ const minScale = options?.minScale ?? 0.1;
1502
+ const tolerance = options?.tolerance ?? 0.01;
1503
+ const maxIterations = options?.maxIterations ?? 50;
1504
+ const maxHeight = config.maxHeight;
1505
+ const maxWidth = config.maxWidth;
1506
+ const origResult = layoutFn(doc);
1507
+ if (origResult.height <= maxHeight && origResult.width <= maxWidth) {
1508
+ return { scaleFactor: 1 };
1509
+ }
1510
+ let lo = minScale;
1511
+ let hi = 1;
1512
+ let best = minScale;
1513
+ for (let iter = 0;iter < maxIterations; iter++) {
1514
+ const mid = (lo + hi) / 2;
1515
+ const scaledDoc = applyScale(doc, mid);
1516
+ const result = layoutFn(scaledDoc);
1517
+ if (result.height <= maxHeight && result.width <= maxWidth) {
1518
+ best = mid;
1519
+ lo = mid + tolerance / 2;
1520
+ } else {
1521
+ hi = mid - tolerance / 2;
1522
+ }
1523
+ if (hi - lo < tolerance)
1524
+ break;
1525
+ }
1526
+ return { scaleFactor: Math.round(best * 100) / 100 };
1527
+ }
1528
+ // src/utils/groupLinesByParagraph.ts
1529
+ function groupLinesByParagraph(lines) {
1530
+ const map = new Map;
1531
+ for (const line of lines) {
1532
+ const idx = line.spans[0]?.pIdx ?? -1;
1533
+ let entry = map.get(idx);
1534
+ if (!entry) {
1535
+ entry = { lines: [] };
1536
+ map.set(idx, entry);
1537
+ }
1538
+ entry.lines.push(line);
1539
+ if (!entry.tag) {
1540
+ entry.tag = line.spans[0]?.tag;
1541
+ }
1542
+ }
1543
+ const groups = [];
1544
+ const sortedKeys = Array.from(map.keys()).sort((a, b) => a - b);
1545
+ for (const key of sortedKeys) {
1546
+ if (key === -1)
1547
+ continue;
1548
+ const entry = map.get(key);
1549
+ groups.push({ lines: entry.lines, pIdx: key, tag: entry.tag });
1550
+ }
1551
+ return groups;
1552
+ }
1553
+ // src/utils/env.ts
1554
+ var _process2 = typeof globalThis !== "undefined" ? globalThis.process : undefined;
1555
+ var isNodeLike = _process2 != null && _process2.versions != null && typeof _process2.versions.node === "string";
1556
+
1557
+ // src/measure/SystemFontRegistry.ts
1558
+ function parseSubfamily(subfamily) {
1559
+ const lower = subfamily.toLowerCase();
1560
+ let style;
1561
+ if (lower.includes("italic")) {
1562
+ style = "italic";
1563
+ } else {
1564
+ style = "normal";
1565
+ }
1566
+ let weight;
1567
+ if (lower.includes("thin") || lower.includes("hairline")) {
1568
+ weight = "thin";
1569
+ } else if (lower.includes("extralight") || lower.includes("ultralight")) {
1570
+ weight = "extralight";
1571
+ } else if (lower.includes("light")) {
1572
+ weight = "light";
1573
+ } else if (lower.includes("semibold") || lower.includes("demibold")) {
1574
+ weight = "semibold";
1575
+ } else if (lower.includes("bold") || lower.includes("heavy") || lower.includes("black")) {
1576
+ weight = "bold";
1577
+ } else if (lower.includes("medium") || lower.includes("medium")) {
1578
+ weight = "medium";
1579
+ } else {
1580
+ weight = "normal";
1581
+ }
1582
+ return { weight, style };
1583
+ }
1584
+
1585
+ class SystemFontRegistry {
1586
+ static _instance;
1587
+ registered = new Map;
1588
+ constructor() {}
1589
+ static get instance() {
1590
+ if (!SystemFontRegistry._instance) {
1591
+ SystemFontRegistry._instance = new SystemFontRegistry;
1592
+ }
1593
+ return SystemFontRegistry._instance;
1594
+ }
1595
+ async scan() {
1596
+ if (!isNodeLike) {
1597
+ console.warn("[vyaz] systemFontRegistry.scan() \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435");
1598
+ return { total: 0, registered: 0 };
1599
+ }
1600
+ const [{ readFileSync }, getSystemFontsModule] = await Promise.all([
1601
+ import("fs"),
1602
+ import("get-system-fonts")
1603
+ ]);
1604
+ const getSystemFonts = getSystemFontsModule.default || getSystemFontsModule;
1605
+ const paths = await getSystemFonts();
1606
+ let registered = 0;
1607
+ for (const fontPath of paths) {
1608
+ try {
1609
+ const buffer = readFileSync(fontPath);
1610
+ const { createFontFace: createFontFace2 } = await Promise.resolve().then(() => exports_FontEngine);
1611
+ const font = await createFontFace2(buffer);
1612
+ const fontkit = font._raw;
1613
+ const family = fontkit.familyName;
1614
+ if (!family)
1615
+ continue;
1616
+ const { weight, style } = parseSubfamily(fontkit.subfamilyName || "Regular");
1617
+ await fontMetricsProvider.registerFont(family, { weight, style }, buffer, fontPath);
1618
+ this.registered.set(family, true);
1619
+ registered++;
1620
+ } catch {
1621
+ continue;
1622
+ }
1623
+ }
1624
+ return { total: paths.length, registered };
1625
+ }
1626
+ isRegistered(family) {
1627
+ return this.registered.has(family);
1628
+ }
1629
+ getRegisteredFamilies() {
1630
+ return Array.from(this.registered.keys());
1631
+ }
1632
+ }
1633
+ var systemFontRegistry = SystemFontRegistry.instance;
1634
+
1635
+ // src/index.ts
1636
+ var DEFAULT_PARAGRAPH_STYLE2 = { ...DEFAULT_PARAGRAPH_STYLE };
1637
+ var DEFAULT_TEXT_STYLE2 = { ...DEFAULT_TEXT_STYLE };
1638
+ var formatListNumber2 = formatListNumber;
1639
+ var defaultBulletChar2 = defaultBulletChar;
1640
+ var BULLET_CHARACTERS2 = { ...BULLET_CHARACTERS };
2
1641
  export {
3
1642
  transformText,
4
1643
  systemFontRegistry,
@@ -13,10 +1652,10 @@ export {
13
1652
  getParagraphText,
14
1653
  getGlyphAdvance,
15
1654
  getFontBuffer,
16
- formatListNumber,
1655
+ formatListNumber2 as formatListNumber,
17
1656
  fontMetricsProvider,
18
1657
  findScale,
19
- defaultBulletChar,
1658
+ defaultBulletChar2 as defaultBulletChar,
20
1659
  createFontFace,
21
1660
  computePixelMetrics,
22
1661
  compileParagraph,
@@ -27,7 +1666,7 @@ export {
27
1666
  ParagraphLayoutEngine,
28
1667
  FontNotFoundError,
29
1668
  FontMetricsProvider,
30
- DEFAULT_TEXT_STYLE,
31
- DEFAULT_PARAGRAPH_STYLE,
32
- BULLET_CHARACTERS
1669
+ DEFAULT_TEXT_STYLE2 as DEFAULT_TEXT_STYLE,
1670
+ DEFAULT_PARAGRAPH_STYLE2 as DEFAULT_PARAGRAPH_STYLE,
1671
+ BULLET_CHARACTERS2 as BULLET_CHARACTERS
33
1672
  };