@vyaz/core 0.0.4 → 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 (42) hide show
  1. package/README.md +156 -0
  2. package/dist/compile/DocumentCompiler.d.ts +40 -0
  3. package/dist/index.browser.d.ts +28 -0
  4. package/dist/index.browser.js +18 -0
  5. package/dist/index.d.ts +30 -0
  6. package/dist/index.js +11 -147930
  7. package/dist/layout/AutoFitEngine.d.ts +44 -0
  8. package/dist/layout/LineBoxValidator.d.ts +25 -0
  9. package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
  10. package/dist/layout/PositioningEngine.d.ts +68 -0
  11. package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
  12. package/dist/layout/estimateWidth.d.ts +36 -0
  13. package/dist/measure/FontEngine.d.ts +47 -0
  14. package/dist/measure/FontMetricsProvider.d.ts +49 -0
  15. package/dist/measure/FontNotFoundError.d.ts +6 -0
  16. package/dist/measure/SystemFontRegistry.d.ts +46 -0
  17. package/dist/measure/canvas-polyfill.d.ts +30 -0
  18. package/dist/types/Document.d.ts +593 -0
  19. package/dist/types/FontTypes.d.ts +61 -0
  20. package/dist/types/LayoutTypes.d.ts +128 -0
  21. package/dist/utils/env.d.ts +11 -0
  22. package/dist/utils/font.d.ts +16 -0
  23. package/dist/utils/groupLinesByParagraph.d.ts +44 -0
  24. package/dist/utils/list.d.ts +41 -0
  25. package/dist/utils/textTransform.d.ts +32 -0
  26. package/package.json +30 -13
  27. package/src/compile/DocumentCompiler.ts +0 -136
  28. package/src/index.ts +0 -80
  29. package/src/layout/AutoFitEngine.ts +0 -101
  30. package/src/layout/LineBoxValidator.ts +0 -162
  31. package/src/layout/ParagraphLayoutEngine.ts +0 -202
  32. package/src/layout/PositioningEngine.ts +0 -401
  33. package/src/layout/TextFrameLayoutEngine.ts +0 -91
  34. package/src/measure/FontMetricsProvider.ts +0 -226
  35. package/src/measure/FontNotFoundError.ts +0 -16
  36. package/src/measure/SystemFontRegistry.ts +0 -151
  37. package/src/measure/canvas-polyfill.d.ts +0 -6
  38. package/src/measure/canvas-polyfill.ts +0 -235
  39. package/src/measure/fontkit.d.ts +0 -44
  40. package/src/types/Document.ts +0 -540
  41. package/src/types/FontTypes.ts +0 -74
  42. package/src/types/LayoutTypes.ts +0 -141
@@ -0,0 +1,44 @@
1
+ /**
2
+ * AutoFitEngine.ts — proportional font-size scaling (autofit).
3
+ *
4
+ * Algorithm: binary search for scale in [0.0, 1.0].
5
+ * For each candidate scale: temporarily scale fontSize,
6
+ * run full layout, check whether the result fits in
7
+ * maxWidth × maxHeight.
8
+ *
9
+ * Proportions are preserved: every TextRun's fontSize is multiplied
10
+ * by the same scale, the visual hierarchy is unchanged.
11
+ * inlineWidget dimensions are NOT scaled (images keep their size).
12
+ */
13
+ import type { TextFrame } from '../types/Document.js';
14
+ /** Autofit options */
15
+ export interface AutoFitOptions {
16
+ minScale?: number;
17
+ tolerance?: number;
18
+ maxIterations?: number;
19
+ }
20
+ /** Autofit result */
21
+ export interface AutoFitResult {
22
+ scaleFactor: number;
23
+ }
24
+ /**
25
+ * Apply a scale factor to all fontSize values in the document.
26
+ * inlineWidget dimensions are NOT scaled.
27
+ * Returns a NEW document (does not mutate the original).
28
+ */
29
+ export declare function applyScale(doc: TextFrame, scale: number): TextFrame;
30
+ /**
31
+ * Find the optimal scale factor for a document.
32
+ *
33
+ * @param doc — source document
34
+ * @param layoutFn — layout(doc) → { height: number; width: number }
35
+ * @param config — autofit maxWidth/maxHeight
36
+ * @param options — search precision
37
+ */
38
+ export declare function findScale(doc: TextFrame, layoutFn: (scaledDoc: TextFrame) => {
39
+ height: number;
40
+ width: number;
41
+ }, config: {
42
+ maxWidth: number;
43
+ maxHeight: number;
44
+ }, options?: AutoFitOptions): AutoFitResult;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * LineBoxValidator.ts — invariant checks and YAML serialization.
3
+ *
4
+ * Parley-inspired invariant checks:
5
+ * NO_OVERLAP, MONOTONIC_Y, INDEX_CONSIST, WIDTH_FIT, BASELINE_EQ
6
+ *
7
+ * YAML snapshots: semantic data only (no metric noise).
8
+ */
9
+ import type { Line } from '../types/LayoutTypes.js';
10
+ export interface InvariantError {
11
+ invariant: string;
12
+ message: string;
13
+ details?: any;
14
+ }
15
+ /**
16
+ * Check all 5 invariants for a Line array.
17
+ * Throws on first violation.
18
+ */
19
+ export declare function assertLineInvariants(lines: Line[], originalText: string, maxWidth: number): void;
20
+ /**
21
+ * Convert Line[] to YAML string for snapshots.
22
+ * Only semantic data: text, x, width, style.
23
+ * No glyphAdvances, fontMetrics (noise), inlineWidget.
24
+ */
25
+ export declare function linesToYAML(lines: Line[], paragraphWidth: number, paragraphHeight: number): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ParagraphLayoutEngine.ts — main orchestrator.
3
+ *
4
+ * Pipeline:
5
+ * Paragraph
6
+ * → compile (DocumentCompiler)
7
+ * → prepareRichInline (pretext)
8
+ * → walkRichInlineLineRanges + materializeRichInlineLineRange (pretext)
9
+ * → positionLines (PositioningEngine)
10
+ * → assertLineInvariants (LineInvariants)
11
+ *
12
+ * Supports autofit via AutoFitEngine.findScale.
13
+ * Caches PreparedRichInline per paragraph key (Parley LayoutContext pattern).
14
+ */
15
+ import '../measure/canvas-polyfill.js';
16
+ import type { Paragraph, ListStyle } from '../types/Document.js';
17
+ import type { IFontMetricsProvider } from '../types/FontTypes.js';
18
+ import type { ParagraphLayoutResult } from '../types/LayoutTypes.js';
19
+ export declare class ParagraphLayoutEngine {
20
+ private preparedCache;
21
+ /**
22
+ * Layout a single paragraph — basic variant.
23
+ *
24
+ * @param paragraph — input paragraph
25
+ * @param maxWidth — available container width (px)
26
+ * @param fontProvider — optional metrics provider (default: fontMetricsProvider)
27
+ * @returns ParagraphLayoutResult with Line[]
28
+ */
29
+ layout(paragraph: Paragraph, maxWidth: number, yOffset?: number, fontProvider?: IFontMetricsProvider, listStyle?: ListStyle, listIndex?: number, listMarkerWidth?: number): ParagraphLayoutResult;
30
+ /**
31
+ * Layout with per-glyph advance widths (for SVG glyph mode).
32
+ *
33
+ * glyphAdvances are now filled by layout() automatically, so
34
+ * this method is equivalent to layout(). Kept for API compatibility.
35
+ */
36
+ layoutGlyph(paragraph: Paragraph, maxWidth: number, yOffset?: number): ParagraphLayoutResult;
37
+ /**
38
+ * Compute per-character advance widths via FontEngine (fontkit).
39
+ * Returns Float32Array for memory efficiency and faster iteration.
40
+ *
41
+ * Throws FontNotFoundError if the font is not registered.
42
+ */
43
+ private computeGlyphAdvances;
44
+ }
45
+ /** Singleton */
46
+ export declare const paragraphLayoutEngine: ParagraphLayoutEngine;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * PositioningEngine.ts — pure X/Y positioning math.
3
+ *
4
+ * Takes pretext output (spans with fragments) + font metrics +
5
+ * paragraph style → returns Line[] with absolute coordinates.
6
+ *
7
+ * X: alignment (left/center/right/justify) + indent
8
+ * Y: baseline + lineHeight + spaceBefore/After
9
+ * Justify: fragmented approach (each space → separate Span)
10
+ *
11
+ * Specs:
12
+ * - CSS Text Module Level 3/4 (browser mode)
13
+ * - ISO/IEC 29500 (Office Open XML / DrawingML, office mode)
14
+ * - Parley alignment.rs (conceptually close, but here justify is simpler:
15
+ * slack is divided equally among stretchable space-spans,
16
+ * without mutating ClusterData.advance)
17
+ */
18
+ import type { ParagraphStyle, ListStyle } from '../types/Document.js';
19
+ import type { FontMetrics } from '../types/FontTypes.js';
20
+ import type { Line } from '../types/LayoutTypes.js';
21
+ import type { PreparedRichInlineItem } from '../compile/DocumentCompiler.js';
22
+ interface PretextFragment {
23
+ itemIndex: number;
24
+ text: string;
25
+ gapBefore: number;
26
+ occupiedWidth: number;
27
+ start: {
28
+ segmentIndex: number;
29
+ graphemeIndex: number;
30
+ };
31
+ end: {
32
+ segmentIndex: number;
33
+ graphemeIndex: number;
34
+ };
35
+ }
36
+ interface PretextLine {
37
+ fragments: PretextFragment[];
38
+ width: number;
39
+ end: {
40
+ segmentIndex: number;
41
+ graphemeIndex: number;
42
+ };
43
+ }
44
+ /**
45
+ * Build Line[] from pretext lines with alignment and metrics.
46
+ *
47
+ * @param pretextLines — pretext result (materializeRichInlineLineRange)
48
+ * @param items — original PreparedRichInlineItem[] (for metadata)
49
+ * @param fontMetricsFn — function to get font metrics for a span
50
+ * @param style — paragraph style
51
+ * @param maxWidth — available container width
52
+ * @param startY — initial Y position
53
+ * @param mode — metric mode ('browser' | 'office'), affects line height calculation
54
+ * @param tag — optional tag for debugging
55
+ * @param measureText — function to measure text width accurately via fontkit.
56
+ * The function accepts (text, fontSize, fontFamily, fontWeight, fontStyle)
57
+ * and returns width in px. Throws FontNotFoundError if font not registered.
58
+ * @param listStyle — optional list configuration (bullet / numbered)
59
+ * @param listIndex — current index in the list (for numbered lists). 1-based.
60
+ * @param listMarkerWidth — pre-computed width of the widest marker in the list group.
61
+ * When provided, bulletIndent is expanded to this value if needed.
62
+ * @returns { lines: Line[], contentWidth: number }
63
+ */
64
+ export declare function positionLines(pretextLines: PretextLine[], items: PreparedRichInlineItem[], fontMetricsFn: (item: PreparedRichInlineItem) => FontMetrics, style: ParagraphStyle, maxWidth: number, startY: number | undefined, mode: "browser" | "office" | undefined, measureText: (text: string, fontSize: number, fontFamily?: string, fontWeight?: string, fontStyle?: string) => number, tag?: string, listStyle?: ListStyle, listIndex?: number, listMarkerWidth?: number): {
65
+ lines: Line[];
66
+ contentWidth: number;
67
+ };
68
+ export {};
@@ -0,0 +1,50 @@
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 } from '../types/Document.js';
24
+ import type { Line } from '../types/LayoutTypes.js';
25
+ /**
26
+ * Result of laying out a full TextFrame.
27
+ *
28
+ * `fitHorizontal` / `fitVertical` tell the renderer which dimension to use:
29
+ * - `'frame'` → use `frameWidth` / `frameHeight`
30
+ * - `'content'` → use `contentWidth` / `contentHeight`
31
+ */
32
+ export interface TextFrameLayoutResult {
33
+ lines: Line[];
34
+ /** Frame width (set when TextFrame.width was provided). */
35
+ frameWidth?: number;
36
+ /** Frame height (set when TextFrame.height was provided). */
37
+ frameHeight?: number;
38
+ /** Actual content width (may exceed frameWidth when wrap=false). */
39
+ contentWidth: number;
40
+ /** Actual content height (may exceed frameHeight). */
41
+ contentHeight: number;
42
+ /** Whether horizontal dimension should use frame or content size. */
43
+ fitHorizontal: 'frame' | 'content';
44
+ /** Whether vertical dimension should use frame or content size. */
45
+ fitVertical: 'frame' | 'content';
46
+ }
47
+ /**
48
+ * Layout a full TextFrame by stacking paragraphs with Y offset accumulation.
49
+ */
50
+ export declare function layoutTextFrame(frame: TextFrame): TextFrameLayoutResult;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * estimateWidth.ts — per-fragment width resolution.
3
+ *
4
+ * Uses exact measurement via FontMetricsProvider.measureText() (fontkit).
5
+ * After measurement — correctToSumInvariant to preserve line-breaking invariant.
6
+ */
7
+ /**
8
+ * Correct fragment widths so that their sum equals occupiedWidth.
9
+ *
10
+ * Why needed: fontkit-based measurement measures each fragment independently,
11
+ * so the sum may drift from occupiedWidth due to:
12
+ * - kerning across fragment boundaries
13
+ * - letterSpacing adjustments from pretext
14
+ * - rounding differences between font parsers
15
+ *
16
+ * The correction distributes delta proportionally to each measured width.
17
+ * For fallback estimates (which already sum to occupiedWidth exactly),
18
+ * this is effectively a no-op.
19
+ */
20
+ export declare function correctToSumInvariant(measured: number[], occupiedWidth: number): number[];
21
+ export type MeasureFn = (text: string) => number;
22
+ /**
23
+ * Resolve widths for fragments of a single text group.
24
+ *
25
+ * Each group consists of pieces split from the same pretext fragment
26
+ * (e.g. leading-space + trimmed-text + trailing-space). Their widths
27
+ * must sum to occupiedWidth (pretext's measurement) to preserve the
28
+ * line-breaking invariant.
29
+ *
30
+ * @param fragments — array of text pieces (e.g. [" ", "between", " form"])
31
+ * @param fullText — concatenation of all fragments (the original pretext fragment text)
32
+ * @param occupiedWidth — total width from pretext (gapBefore + textWidth)
33
+ * @param measureFn — optional callback for exact measurement via font metrics provider
34
+ * @returns widths that sum to occupiedWidth (within floating point tolerance)
35
+ */
36
+ export declare function resolveFragmentWidths(fragments: string[], fullText: string, occupiedWidth: number, measureFn: MeasureFn): number[];
@@ -0,0 +1,47 @@
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
+ import type { FontMetrics } from '../types/FontTypes.js';
14
+ /** Opaque font face handle returned by FontEngine.create() */
15
+ export interface FontFace {
16
+ /** fontkit font object (private — not meant for direct access) */
17
+ readonly _raw: any;
18
+ /** Cached values extracted once after creation */
19
+ readonly unitsPerEm: number;
20
+ readonly ascent: number;
21
+ readonly descent: number;
22
+ readonly capHeight: number;
23
+ readonly winAscent: number | null;
24
+ readonly winDescent: number | null;
25
+ }
26
+ /**
27
+ * Create a font face from a binary buffer.
28
+ *
29
+ * @param buffer Font file bytes (ArrayBuffer in browser, Uint8Array/Buffer in Node.js)
30
+ * @returns Opaque FontFace handle
31
+ */
32
+ export declare function createFontFace(buffer: ArrayBuffer | Uint8Array): Promise<FontFace>;
33
+ /**
34
+ * Get the advance width (in font units) for a single code point.
35
+ *
36
+ * @returns advance width in font units, or `null` if the glyph is missing
37
+ */
38
+ export declare function getGlyphAdvance(font: FontFace, codePoint: number): number | null;
39
+ /**
40
+ * Compute pixel‑scale metrics for a given font size.
41
+ */
42
+ export declare function computePixelMetrics(font: FontFace, fontSize: number, mode: 'browser' | 'office'): FontMetrics;
43
+ /**
44
+ * Whether the fontkit module was successfully loaded.
45
+ * Useful for tests to verify the bundler isn't blocking fontkit.
46
+ */
47
+ export declare function isFontEngineAvailable(): Promise<boolean>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * FontMetricsProvider.ts — isomorphic font metrics provider.
3
+ *
4
+ * Strategy (priority):
5
+ * 1. FontEngine (fontkit) — from registered buffer
6
+ * - 'browser' mode: hhea.ascender / hhea.descender
7
+ * - 'office' mode: OS/2.usWinAscent / OS/2.usWinDescent
8
+ * 2. Canvas TextMetrics (browser fallback when fontkit unavailable)
9
+ *
10
+ * Uses FontEngine as the single entry point for all fontkit operations.
11
+ */
12
+ import type { FontMetrics, IFontMetricsProvider } from '../types/FontTypes.js';
13
+ import type { FontFace } from './FontEngine.js';
14
+ /**
15
+ * Factor used when a glyph is not found in the font.
16
+ * Multiplied by fontSize to estimate the missing glyph width.
17
+ * Used across all measurement code paths (ParagraphLayoutEngine, canvas-polyfill).
18
+ */
19
+ export declare const MISSING_GLYPH_FACTOR = 0.5;
20
+ export declare class FontMetricsProvider implements IFontMetricsProvider {
21
+ /** Map<string, FontFace> — font engine font face cache */
22
+ private cache;
23
+ private metricsCache;
24
+ private mode;
25
+ setMode(mode: 'browser' | 'office'): void;
26
+ getMode(): 'browser' | 'office';
27
+ /**
28
+ * Register a binary font for use with fontkit.
29
+ *
30
+ * In both Node.js and browser the font is loaded via FontEngine.
31
+ * In the browser the caller must provide font bytes (e.g. fetched via
32
+ * `getFontBuffer()` from `../utils/font.js`).
33
+ *
34
+ * @param source Font file bytes (ArrayBuffer / Uint8Array), or a URL string
35
+ * @param sourcePath Optional filesystem path (used for @napi-rs/canvas in Node.js)
36
+ */
37
+ registerFont(family: string, options: {
38
+ weight?: string;
39
+ style?: string;
40
+ }, source: string | ArrayBuffer | Uint8Array, sourcePath?: string): Promise<void>;
41
+ /**
42
+ * Get font engine FontFace object for per-character calculations.
43
+ * Returns undefined if font is not registered.
44
+ */
45
+ getFont(family: string, weight?: string, style?: string): FontFace | undefined;
46
+ getMetrics(fontFamily: string, fontSize: number, weight?: string, style?: string): FontMetrics;
47
+ }
48
+ /** Singleton */
49
+ export declare const fontMetricsProvider: FontMetricsProvider;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * FontNotFoundError.ts — thrown when a requested font is not registered.
3
+ */
4
+ export declare class FontNotFoundError extends Error {
5
+ constructor(family: string, weight?: string, style?: string);
6
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * SystemFontRegistry.ts — singleton that scans system fonts using `get-system-fonts`
3
+ * and registers them in FontMetricsProvider for both fontkit and @napi-rs/canvas.
4
+ *
5
+ * Usage:
6
+ * import { systemFontRegistry } from './SystemFontRegistry.js';
7
+ * await systemFontRegistry.scan();
8
+ * console.log(systemFontRegistry.getRegisteredFamilies());
9
+ *
10
+ * ⚠️ Node.js built-in module imports are dynamic (lazy) to avoid
11
+ * Vite/Webpack externalization errors in browser builds.
12
+ */
13
+ interface ScanResult {
14
+ /** Total font files found on the system */
15
+ total: number;
16
+ /** Number of successfully registered font files */
17
+ registered: number;
18
+ }
19
+ export declare class SystemFontRegistry {
20
+ private static _instance;
21
+ /** Map of registered family names → true */
22
+ private registered;
23
+ private constructor();
24
+ static get instance(): SystemFontRegistry;
25
+ /**
26
+ * Scan the system for all fonts and register them in FontMetricsProvider.
27
+ *
28
+ * - Uses `get-system-fonts` to find all .ttf/.otf files
29
+ * - Opens each with fontkit to extract familyName/subfamilyName
30
+ * - Registers in fontMetricsProvider (fontkit buffer + canvas path)
31
+ *
32
+ * @returns stats about what was found and registered
33
+ */
34
+ scan(): Promise<ScanResult>;
35
+ /**
36
+ * Check if a font family is registered.
37
+ */
38
+ isRegistered(family: string): boolean;
39
+ /**
40
+ * Get list of all registered font families.
41
+ */
42
+ getRegisteredFamilies(): string[];
43
+ }
44
+ /** Singleton instance */
45
+ export declare const systemFontRegistry: SystemFontRegistry;
46
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Canvas API polyfill for Bun/Node.js via @napi-rs/canvas.
3
+ *
4
+ * Required by @chenglou/pretext in server environments (Bun/Node.js without DOM).
5
+ *
6
+ * Two levels of polyfill:
7
+ * 1. globalThis.document.createElement('canvas') — pretext uses this
8
+ * inside prepare() to create a temporary Canvas and call measureText.
9
+ * 2. OffscreenCanvas — for some libraries and early versions.
10
+ *
11
+ * Exports enableOfficeTextMeasure / disableOfficeTextMeasure —
12
+ * ctx.measureText override for FontEngine-based measurements in Office mode.
13
+ *
14
+ * ⚠️ All Node.js built-in module imports are dynamic (lazy) to avoid
15
+ * Vite/Webpack externalization errors in browser builds.
16
+ */
17
+ /**
18
+ * Register a font with @napi-rs/canvas so ctx.measureText() works in Node.js.
19
+ * No-op in browser or when @napi-rs/canvas is not available.
20
+ */
21
+ export declare function registerCanvasFont(fontPath: string, family: string): void;
22
+ /**
23
+ * Enable Office measurement: replaces ctx.measureText with FontEngine-based version.
24
+ * @param fontCache — Map<family_weight_style, FontFace> from FontMetricsProvider
25
+ */
26
+ export declare function enableOfficeTextMeasure(fontCache: Map<string, any>): void;
27
+ /**
28
+ * Restore original ctx.measureText.
29
+ */
30
+ export declare function disableOfficeTextMeasure(): void;