@vyaz/core 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/compile/DocumentCompiler.d.ts +40 -0
  2. package/dist/index.browser.d.ts +28 -0
  3. package/dist/index.browser.js +18 -0
  4. package/dist/index.d.ts +30 -0
  5. package/dist/index.js +11 -147930
  6. package/dist/layout/AutoFitEngine.d.ts +44 -0
  7. package/dist/layout/LineBoxValidator.d.ts +25 -0
  8. package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
  9. package/dist/layout/PositioningEngine.d.ts +68 -0
  10. package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
  11. package/{src/layout/estimateWidth.ts → dist/layout/estimateWidth.d.ts} +2 -40
  12. package/dist/measure/FontEngine.d.ts +47 -0
  13. package/dist/measure/FontMetricsProvider.d.ts +49 -0
  14. package/dist/measure/FontNotFoundError.d.ts +6 -0
  15. package/dist/measure/SystemFontRegistry.d.ts +46 -0
  16. package/dist/measure/canvas-polyfill.d.ts +30 -0
  17. package/dist/types/Document.d.ts +593 -0
  18. package/dist/types/FontTypes.d.ts +61 -0
  19. package/dist/types/LayoutTypes.d.ts +128 -0
  20. package/{src/utils/env.ts → dist/utils/env.d.ts} +1 -8
  21. package/{src/utils/font.ts → dist/utils/font.d.ts} +1 -13
  22. package/{src/utils/groupLinesByParagraph.ts → dist/utils/groupLinesByParagraph.d.ts} +7 -37
  23. package/dist/utils/list.d.ts +41 -0
  24. package/dist/utils/textTransform.d.ts +32 -0
  25. package/package.json +13 -13
  26. package/src/compile/DocumentCompiler.ts +0 -144
  27. package/src/index.browser.ts +0 -90
  28. package/src/index.ts +0 -97
  29. package/src/layout/AutoFitEngine.ts +0 -101
  30. package/src/layout/LineBoxValidator.ts +0 -162
  31. package/src/layout/ParagraphLayoutEngine.ts +0 -264
  32. package/src/layout/PositioningEngine.ts +0 -615
  33. package/src/layout/TextFrameLayoutEngine.ts +0 -363
  34. package/src/measure/FontEngine.ts +0 -144
  35. package/src/measure/FontMetricsProvider.ts +0 -224
  36. package/src/measure/FontNotFoundError.ts +0 -16
  37. package/src/measure/SystemFontRegistry.ts +0 -152
  38. package/src/measure/canvas-polyfill.d.ts +0 -6
  39. package/src/measure/canvas-polyfill.ts +0 -243
  40. package/src/measure/fontkit.d.ts +0 -44
  41. package/src/types/Document.ts +0 -666
  42. package/src/types/FontTypes.ts +0 -74
  43. package/src/types/LayoutTypes.ts +0 -158
  44. package/src/utils/list.ts +0 -107
  45. package/src/utils/textTransform.ts +0 -96
@@ -1,363 +0,0 @@
1
- /**
2
- * TextFrameLayoutEngine.ts — Layout a full TextFrame (multi-paragraph).
3
- *
4
- * Pipeline:
5
- * TextFrame → Paragraph[] → paragraphLayoutEngine.layout() each → merge Line[]
6
- *
7
- * Handles:
8
- * - Paragraph stacking with Y offset accumulation
9
- * - Multi-column layout (CSS multi-column model)
10
- * - Padding (left reduces available width, left shifts X)
11
- * - frame.width/height optional → fitHorizontal/fitVertical flags
12
- * - List grouping: consecutive paragraphs with listStyle form a list group.
13
- * Numbered list indices are auto-incremented within each group.
14
- * `listRestart: true` breaks a group and restarts numbering.
15
- *
16
- * Multi-column algorithm:
17
- * 1. Calculate colWidth = (frameWidth - (count-1)*gap - padding) / count
18
- * 2. Layout each paragraph with maxWidth = colWidth (NOT frame.width)
19
- * 3. Distribute lines column-by-column (column-fill: auto)
20
- * 4. If frame.height is set, lines overflow to next column when colHeight exceeded
21
- * 5. If no frame.height, columns are infinite (all lines stay in column 0)
22
- */
23
- import type { TextFrame, ListStyle, VerticalAlignment } from '../types/Document.js';
24
- import type { Line } from '../types/LayoutTypes.js';
25
- import { paragraphLayoutEngine } from './ParagraphLayoutEngine.js';
26
- import { formatListNumber, defaultBulletChar } from '../utils/list.js';
27
-
28
- /**
29
- * Result of laying out a full TextFrame.
30
- *
31
- * `fitHorizontal` / `fitVertical` tell the renderer which dimension to use:
32
- * - `'frame'` → use `frameWidth` / `frameHeight`
33
- * - `'content'` → use `contentWidth` / `contentHeight`
34
- */
35
- export interface TextFrameLayoutResult {
36
- lines: Line[];
37
- /** Frame width (set when TextFrame.width was provided). */
38
- frameWidth?: number;
39
- /** Frame height (set when TextFrame.height was provided). */
40
- frameHeight?: number;
41
- /** Actual content width (may exceed frameWidth when wrap=false). */
42
- contentWidth: number;
43
- /** Actual content height (may exceed frameHeight). */
44
- contentHeight: number;
45
- /** Whether horizontal dimension should use frame or content size. */
46
- fitHorizontal: 'frame' | 'content';
47
- /** Whether vertical dimension should use frame or content size. */
48
- fitVertical: 'frame' | 'content';
49
- }
50
-
51
- /**
52
- * Resolve the marker text for a list item (needed for width measurement).
53
- */
54
- function getMarkerTextHelper(listStyle: ListStyle, listIndex: number): string {
55
- if (listStyle.type === 'bullet') {
56
- return listStyle.bulletChar ?? defaultBulletChar(listStyle.level ?? 0);
57
- }
58
- if (listStyle.type === 'number') {
59
- const fmt = listStyle.numberFormat ?? 'decimal';
60
- return formatListNumber(listIndex, fmt) + '.';
61
- }
62
- return '';
63
- }
64
-
65
- /**
66
- * Compute the widest marker across a list group, used to expand bulletIndent
67
- * when numbered markers have varying widths (e.g. "9." vs "10.").
68
- */
69
- function computeMaxMarkerWidth(
70
- listStyle: ListStyle,
71
- startIndex: number,
72
- count: number,
73
- measureText: (text: string, fontSize: number) => number,
74
- ): number {
75
- if (listStyle.type !== 'number') return 0;
76
- let maxWidth = 0;
77
- for (let i = 0; i < count; i++) {
78
- const markerText = getMarkerTextHelper(listStyle, startIndex + i);
79
- const width = measureText(markerText, 12); // approximate, will be refined by positionLines
80
- maxWidth = Math.max(maxWidth, width);
81
- }
82
- return maxWidth;
83
- }
84
-
85
- /**
86
- * Apply vertical alignment to lines within a column.
87
- *
88
- * @param lines — lines belonging to this column (already has correct x/y)
89
- * @param colHeight — total column height (frame.height or content height)
90
- */
91
- function applyVerticalAlignment(
92
- lines: Line[],
93
- colHeight: number,
94
- alignment: VerticalAlignment,
95
- ): void {
96
- if (alignment === 'top' || lines.length === 0) return;
97
-
98
- const firstLineY = lines[0].y;
99
- const lastLineEnd = lines[lines.length - 1].y + lines[lines.length - 1].height;
100
- const contentHeight = lastLineEnd - firstLineY;
101
- const extraSpace = colHeight - contentHeight;
102
- if (extraSpace <= 0) return;
103
-
104
- let offset = 0;
105
- if (alignment === 'middle') {
106
- offset = extraSpace / 2;
107
- } else if (alignment === 'bottom') {
108
- offset = extraSpace;
109
- }
110
-
111
- for (const line of lines) {
112
- line.y += offset;
113
- }
114
- }
115
-
116
- /**
117
- * Layout a full TextFrame by stacking paragraphs with Y offset accumulation.
118
- */
119
- export function layoutTextFrame(frame: TextFrame): TextFrameLayoutResult {
120
- // ── Multi-column setup ────────────────────────────────────────────
121
- const leftPad = frame.padding?.left ?? 0;
122
- const rightPad = frame.padding?.right ?? 0;
123
- const topPad = frame.padding?.top ?? 0;
124
- const bottomPad = frame.padding?.bottom ?? 0;
125
-
126
- const hasColumns = frame.columns != null && frame.columns.count > 1 && frame.width != null;
127
- let colWidth: number | undefined;
128
- let colCount = 1;
129
- let colGap = 0;
130
-
131
- if (hasColumns) {
132
- colCount = frame.columns!.count;
133
- colGap = frame.columns!.gap;
134
- const totalPad = leftPad + rightPad + (colCount - 1) * colGap;
135
- colWidth = (frame.width! - totalPad) / colCount;
136
- }
137
-
138
- const colHeight = frame.height != null
139
- ? frame.height - topPad - bottomPad
140
- : Infinity;
141
-
142
- const verticalAlign: VerticalAlignment = frame.verticalAlignment ?? 'top';
143
-
144
- // ── List grouping pass ──────────────────────────────────────────
145
- // (identical to before, but uses colWidth for maxWidth later)
146
- const listIndices: (number | undefined)[] = new Array(frame.paragraphs.length).fill(undefined);
147
- const listMarkerWidths: (number | undefined)[] = new Array(frame.paragraphs.length).fill(undefined);
148
-
149
- let i = 0;
150
- while (i < frame.paragraphs.length) {
151
- const p = frame.paragraphs[i];
152
- const ls = p.style.listStyle;
153
-
154
- if (!ls || ls.type === 'none') {
155
- i++;
156
- continue;
157
- }
158
-
159
- // Find end of this list group
160
- let groupStart = i;
161
- let groupEnd = i + 1;
162
- while (groupEnd < frame.paragraphs.length) {
163
- const nextP = frame.paragraphs[groupEnd];
164
- const nextLs = nextP.style.listStyle;
165
- if (!nextLs || nextLs.type !== ls.type || nextP.style.listRestart) {
166
- break;
167
- }
168
- // Same nesting level only
169
- if ((nextLs.level ?? 0) !== (ls.level ?? 0)) {
170
- break;
171
- }
172
- groupEnd++;
173
- }
174
-
175
- const groupSize = groupEnd - groupStart;
176
- const startNumber = ls.startNumber ?? 1;
177
-
178
- // Assign indices
179
- for (let j = 0; j < groupSize; j++) {
180
- listIndices[groupStart + j] = startNumber + j;
181
- }
182
-
183
- // Compute max marker width for numbered lists in this group
184
- const paraFontSize = p.children[0]?.fontSize ?? 12;
185
- const measureMarkerWidth = (text: string, fontSize: number): number => {
186
- return text.length * fontSize * 0.6;
187
- };
188
- const maxMW = computeMaxMarkerWidth(ls, startNumber, groupSize, measureMarkerWidth);
189
- for (let j = 0; j < groupSize; j++) {
190
- listMarkerWidths[groupStart + j] = maxMW;
191
- }
192
-
193
- i = groupEnd;
194
- }
195
-
196
- // ── Layout pass ─────────────────────────────────────────────────
197
- const allLines: Line[] = [];
198
- let contentWidth = 0;
199
-
200
- // Helper: push a line and update position
201
- const currentColY: number[] = new Array(colCount).fill(topPad);
202
-
203
- // For non-column layout, we use a single "virtual column" approach
204
- for (let i = 0; i < frame.paragraphs.length; i++) {
205
- const p = frame.paragraphs[i];
206
-
207
- // Available width: colWidth if columns, otherwise frame.width minus padding
208
- const maxWidth = hasColumns
209
- ? colWidth!
210
- : frame.width !== undefined
211
- ? frame.width - leftPad - rightPad
212
- : Infinity;
213
-
214
- // If wrap is disabled, force no-wrap on the paragraph
215
- if (frame.wrap === false) {
216
- p.style = { ...p.style, whiteSpace: 'nowrap' };
217
- }
218
-
219
- const listIndex = listIndices[i];
220
- const listMarkerWidth = listMarkerWidths[i];
221
- const listStyle = p.style.listStyle;
222
-
223
- // Start paragraph on current column (column 0 initially, or current active column)
224
- // We layout the paragraph with the full maxWidth — the paragraph's own
225
- // line-breaking handles wrapping.
226
- // For multi-column, we use a relative yOffset = 0, then position lines below.
227
- const result = paragraphLayoutEngine.layout(
228
- p,
229
- maxWidth,
230
- 0, // relative yOffset — we'll position lines ourselves
231
- undefined,
232
- listStyle,
233
- listIndex,
234
- listMarkerWidth,
235
- );
236
-
237
- // Apply paragraph-level spaceBefore (CSS margin-top equivalent).
238
- // positionLines() already offsets Y by spaceBefore internally, but
239
- // line.y is overwritten below with the global Y from currentColY.
240
- // So we must add spaceBefore to currentColY before placing lines.
241
- if (!hasColumns) {
242
- currentColY[0] += p.style.spaceBefore;
243
- }
244
-
245
- for (const line of result.lines) {
246
- if (!hasColumns) {
247
- // Non-column: simple accumulation (existing behavior)
248
- line.x += leftPad;
249
- for (const span of line.spans) {
250
- span.pIdx = i;
251
- }
252
- allLines.push(line);
253
- contentWidth = Math.max(contentWidth, result.contentWidth);
254
- // Result height already includes the passed yOffset (0), so this is relative.
255
- // We accumulate absolute y from result.height (which is total paragraph height).
256
- line.y = currentColY[0];
257
- currentColY[0] += line.height;
258
- continue;
259
- }
260
-
261
- // ── Multi-column: distribute lines across columns ──────────
262
- // Try to place the current line in the current column.
263
- // If it doesn't fit — move to next column.
264
- let colIdx = 0;
265
- for (let c = 0; c < colCount; c++) {
266
- if (currentColY[c] < currentColY[colIdx]) colIdx = c;
267
- }
268
-
269
- // Try current column; if line doesn't fit, advance to next.
270
- // For auto fill: each column fills completely before moving to next.
271
- // We use a greedy column selection: find the column with smallest Y
272
- // that has room for this line.
273
- let placed = false;
274
- for (let attempt = 0; attempt < colCount; attempt++) {
275
- if (currentColY[colIdx] + line.height <= colHeight) {
276
- // Fits in this column
277
- line.x = colIdx * (colWidth! + colGap) + leftPad;
278
- line.y = currentColY[colIdx];
279
- line.columnIndex = colIdx;
280
- currentColY[colIdx] += line.height;
281
- for (const span of line.spans) {
282
- span.pIdx = i;
283
- }
284
- allLines.push(line);
285
- contentWidth = Math.max(contentWidth, line.x + line.width + rightPad);
286
- placed = true;
287
- break;
288
- }
289
- // Advance to next column
290
- colIdx = (colIdx + 1) % colCount;
291
-
292
- // If we've wrapped around, all columns are full — overflow stays in last column
293
- if (attempt === colCount - 1) {
294
- // Place in last attempted column even if it overflows
295
- line.x = colIdx * (colWidth! + colGap) + leftPad;
296
- line.y = currentColY[colIdx];
297
- line.columnIndex = colIdx;
298
- currentColY[colIdx] += line.height;
299
- for (const span of line.spans) {
300
- span.pIdx = i;
301
- }
302
- allLines.push(line);
303
- contentWidth = Math.max(contentWidth, line.x + line.width + rightPad);
304
- placed = true;
305
- }
306
- }
307
-
308
- if (!placed) {
309
- // Fallback: place in column 0 (shouldn't happen)
310
- line.x = leftPad;
311
- line.y = currentColY[0];
312
- line.columnIndex = 0;
313
- currentColY[0] += line.height;
314
- for (const span of line.spans) {
315
- span.pIdx = i;
316
- }
317
- allLines.push(line);
318
- }
319
- }
320
-
321
- // Apply paragraph-level spaceAfter (CSS margin-bottom equivalent).
322
- // positionLines() in PositioningEngine applies spaceBefore but does NOT
323
- // add spaceAfter — it is the caller's responsibility.
324
- if (!hasColumns) {
325
- currentColY[0] += p.style.spaceAfter;
326
- }
327
- }
328
-
329
- // ── Apply vertical alignment per column ──────────────────────────
330
- if (hasColumns && verticalAlign !== 'top' && frame.height != null) {
331
- const colLines: Line[][] = new Array(colCount).fill(null).map(() => []);
332
- for (const line of allLines) {
333
- const ci = line.columnIndex ?? 0;
334
- colLines[ci].push(line);
335
- }
336
- for (let c = 0; c < colCount; c++) {
337
- applyVerticalAlignment(colLines[c], colHeight, verticalAlign);
338
- }
339
- }
340
-
341
- // ── Compute final content dimensions ─────────────────────────────
342
- if (hasColumns) {
343
- // contentWidth = total frame width (includes all columns + gaps + padding)
344
- contentWidth = frame.width!;
345
- } else {
346
- contentWidth += rightPad;
347
- }
348
-
349
- const lastLine = allLines.length > 0 ? allLines[allLines.length - 1] : null;
350
- const contentHeight = lastLine
351
- ? lastLine.y + lastLine.height + bottomPad
352
- : bottomPad;
353
-
354
- return {
355
- lines: allLines,
356
- frameWidth: frame.width,
357
- frameHeight: frame.height,
358
- contentWidth,
359
- contentHeight,
360
- fitHorizontal: frame.width !== undefined ? 'frame' : 'content',
361
- fitVertical: frame.height !== undefined ? 'frame' : 'content',
362
- };
363
- }
@@ -1,144 +0,0 @@
1
- /**
2
- * FontEngine.ts — unified facade over fontkit.
3
- *
4
- * Is the single entry point for all fontkit operations:
5
- * - create(buffer) → font face
6
- * - getGlyphAdvance(font, codePoint) → per‑glyph advance
7
- * - getMetrics(font) → structured metric values
8
- *
9
- * fontkit works in both Node.js (native addon) and browser (dist/browser-module.mjs).
10
- * Bundlers pick the correct entry automatically when `package.json` browser map
11
- * is removed (or when the import is not blocked by stubs).
12
- */
13
-
14
- import type { FontMetrics } from '../types/FontTypes.js';
15
-
16
- // ── Internal font object shape ─────────────────────────────────────────
17
- // We keep fontkit.Font opaque — users of FontEngine never import fontkit.
18
-
19
- /** Opaque font face handle returned by FontEngine.create() */
20
- export interface FontFace {
21
- /** fontkit font object (private — not meant for direct access) */
22
- readonly _raw: any;
23
- /** Cached values extracted once after creation */
24
- readonly unitsPerEm: number;
25
- readonly ascent: number;
26
- readonly descent: number;
27
- readonly capHeight: number;
28
- readonly winAscent: number | null;
29
- readonly winDescent: number | null;
30
- }
31
-
32
- // ── FontEngine ─────────────────────────────────────────────────────────
33
-
34
- let _fontkitModule: any | null = null;
35
-
36
- /**
37
- * Lazily import fontkit (avoids top‑level side‑effects in bundlers).
38
- * fontkit exposes both CJS and ESM browser builds — bundlers resolve
39
- * the correct entry from package.json exports.
40
- */
41
- async function _getFontkit(): Promise<any> {
42
- if (_fontkitModule) return _fontkitModule;
43
- const mod = await import('fontkit');
44
- _fontkitModule = mod.default || mod;
45
- return _fontkitModule;
46
- }
47
-
48
- /**
49
- * Extract metric values from a raw fontkit font object.
50
- */
51
- function _extractMetrics(raw: any): {
52
- unitsPerEm: number;
53
- ascent: number;
54
- descent: number;
55
- capHeight: number;
56
- winAscent: number | null;
57
- winDescent: number | null;
58
- } {
59
- const os2 = raw['OS/2'];
60
- return {
61
- unitsPerEm: raw.unitsPerEm,
62
- ascent: raw.ascent,
63
- descent: raw.descent,
64
- capHeight: raw.capHeight ?? raw.ascent,
65
- winAscent: os2?.winAscent ?? null,
66
- winDescent: os2?.winDescent ?? null,
67
- };
68
- }
69
-
70
- /**
71
- * Get a glyph handle for a code point.
72
- * Returns null when the glyph is not present (e.g. .notdef).
73
- */
74
- function _getGlyph(raw: any, codePoint: number): any | null {
75
- return raw.glyphForCodePoint(codePoint) ?? null;
76
- }
77
-
78
- // ── Public API ─────────────────────────────────────────────────────────
79
-
80
- /**
81
- * Create a font face from a binary buffer.
82
- *
83
- * @param buffer Font file bytes (ArrayBuffer in browser, Uint8Array/Buffer in Node.js)
84
- * @returns Opaque FontFace handle
85
- */
86
- export async function createFontFace(buffer: ArrayBuffer | Uint8Array): Promise<FontFace> {
87
- const fontkit = await _getFontkit();
88
- const raw = fontkit.create(buffer);
89
- const metrics = _extractMetrics(raw);
90
- return {
91
- _raw: raw,
92
- ...metrics,
93
- };
94
- }
95
-
96
- /**
97
- * Get the advance width (in font units) for a single code point.
98
- *
99
- * @returns advance width in font units, or `null` if the glyph is missing
100
- */
101
- export function getGlyphAdvance(font: FontFace, codePoint: number): number | null {
102
- const glyph = _getGlyph(font._raw, codePoint);
103
- if (!glyph) return null;
104
- return glyph.advanceWidth;
105
- }
106
-
107
- /**
108
- * Compute pixel‑scale metrics for a given font size.
109
- */
110
- export function computePixelMetrics(font: FontFace, fontSize: number, mode: 'browser' | 'office'): FontMetrics {
111
- const scale = fontSize / font.unitsPerEm;
112
-
113
- if (mode === 'office' && font.winAscent != null && font.winDescent != null) {
114
- return {
115
- ascent: font.winAscent * scale * 1.078,
116
- descent: Math.abs(font.winDescent) * scale * 1.078,
117
- capHeight: (font.capHeight ?? font.ascent) * scale,
118
- unitsPerEm: font.unitsPerEm,
119
- sourceTable: 'OS/2',
120
- };
121
- }
122
-
123
- // browser mode (or Office fallback when OS/2 is absent)
124
- return {
125
- ascent: font.ascent * scale,
126
- descent: Math.abs(font.descent) * scale,
127
- capHeight: (font.capHeight ?? font.ascent) * scale,
128
- unitsPerEm: font.unitsPerEm,
129
- sourceTable: 'hhea',
130
- };
131
- }
132
-
133
- /**
134
- * Whether the fontkit module was successfully loaded.
135
- * Useful for tests to verify the bundler isn't blocking fontkit.
136
- */
137
- export async function isFontEngineAvailable(): Promise<boolean> {
138
- try {
139
- const fk = await _getFontkit();
140
- return typeof fk.create === 'function';
141
- } catch {
142
- return false;
143
- }
144
- }