@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.
- package/README.md +156 -0
- package/dist/compile/DocumentCompiler.d.ts +40 -0
- package/dist/index.browser.d.ts +28 -0
- package/dist/index.browser.js +18 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +11 -147930
- package/dist/layout/AutoFitEngine.d.ts +44 -0
- package/dist/layout/LineBoxValidator.d.ts +25 -0
- package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
- package/dist/layout/PositioningEngine.d.ts +68 -0
- package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
- package/dist/layout/estimateWidth.d.ts +36 -0
- package/dist/measure/FontEngine.d.ts +47 -0
- package/dist/measure/FontMetricsProvider.d.ts +49 -0
- package/dist/measure/FontNotFoundError.d.ts +6 -0
- package/dist/measure/SystemFontRegistry.d.ts +46 -0
- package/dist/measure/canvas-polyfill.d.ts +30 -0
- package/dist/types/Document.d.ts +593 -0
- package/dist/types/FontTypes.d.ts +61 -0
- package/dist/types/LayoutTypes.d.ts +128 -0
- package/dist/utils/env.d.ts +11 -0
- package/dist/utils/font.d.ts +16 -0
- package/dist/utils/groupLinesByParagraph.d.ts +44 -0
- package/dist/utils/list.d.ts +41 -0
- package/dist/utils/textTransform.d.ts +32 -0
- package/package.json +30 -13
- package/src/compile/DocumentCompiler.ts +0 -136
- package/src/index.ts +0 -80
- package/src/layout/AutoFitEngine.ts +0 -101
- package/src/layout/LineBoxValidator.ts +0 -162
- package/src/layout/ParagraphLayoutEngine.ts +0 -202
- package/src/layout/PositioningEngine.ts +0 -401
- package/src/layout/TextFrameLayoutEngine.ts +0 -91
- package/src/measure/FontMetricsProvider.ts +0 -226
- package/src/measure/FontNotFoundError.ts +0 -16
- package/src/measure/SystemFontRegistry.ts +0 -151
- package/src/measure/canvas-polyfill.d.ts +0 -6
- package/src/measure/canvas-polyfill.ts +0 -235
- package/src/measure/fontkit.d.ts +0 -44
- package/src/types/Document.ts +0 -540
- package/src/types/FontTypes.ts +0 -74
- package/src/types/LayoutTypes.ts +0 -141
|
@@ -1,162 +0,0 @@
|
|
|
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
|
-
|
|
10
|
-
import { dump } from 'js-yaml';
|
|
11
|
-
import type { Line, Span, SemanticParagraph } from '../types/LayoutTypes.js';
|
|
12
|
-
|
|
13
|
-
const EPSILON = 0.5; // subpixel tolerance
|
|
14
|
-
|
|
15
|
-
// ── Invariant guard ────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export interface InvariantError {
|
|
18
|
-
invariant: string;
|
|
19
|
-
message: string;
|
|
20
|
-
details?: any;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check all 5 invariants for a Line array.
|
|
25
|
-
* Throws on first violation.
|
|
26
|
-
*/
|
|
27
|
-
export function assertLineInvariants(
|
|
28
|
-
lines: Line[],
|
|
29
|
-
originalText: string,
|
|
30
|
-
maxWidth: number,
|
|
31
|
-
): void {
|
|
32
|
-
if (lines.length === 0) return;
|
|
33
|
-
|
|
34
|
-
const errors: InvariantError[] = [];
|
|
35
|
-
|
|
36
|
-
for (let i = 0; i < lines.length; i++) {
|
|
37
|
-
const line = lines[i];
|
|
38
|
-
|
|
39
|
-
// 1. NO_OVERLAP: lines[i+1].y >= lines[i].y + lines[i].height
|
|
40
|
-
if (i > 0) {
|
|
41
|
-
const prev = lines[i - 1];
|
|
42
|
-
if (line.y < prev.y + prev.height - EPSILON) {
|
|
43
|
-
errors.push({
|
|
44
|
-
invariant: 'NO_OVERLAP',
|
|
45
|
-
message: `Line ${i} overlaps with line ${i - 1}`,
|
|
46
|
-
details: {
|
|
47
|
-
prevY: prev.y,
|
|
48
|
-
prevHeight: prev.height,
|
|
49
|
-
prevBottom: prev.y + prev.height,
|
|
50
|
-
currentY: line.y,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 2. MONOTONIC_Y: lines[i+1].y > lines[i].y
|
|
57
|
-
if (i > 0 && line.y <= lines[i - 1].y) {
|
|
58
|
-
errors.push({
|
|
59
|
-
invariant: 'MONOTONIC_Y',
|
|
60
|
-
message: `Line ${i} has Y=${line.y} not > prev Y=${lines[i - 1].y}`,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 3. INDEX_CONSIST: checked below (sum of all lengths)
|
|
65
|
-
if (line.endIndex <= line.startIndex) {
|
|
66
|
-
errors.push({
|
|
67
|
-
invariant: 'INDEX_CONSIST',
|
|
68
|
-
message: `Line ${i}: endIndex=${line.endIndex} <= startIndex=${line.startIndex}`,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 4. WIDTH_FIT: line.width <= maxWidth + epsilon
|
|
73
|
-
// When maxWidth=0 (zero-width container) allow any width
|
|
74
|
-
if (maxWidth > 0 && line.width > maxWidth + EPSILON) {
|
|
75
|
-
errors.push({
|
|
76
|
-
invariant: 'WIDTH_FIT',
|
|
77
|
-
message: `Line ${i}: width=${line.width} > maxWidth=${maxWidth}`,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 5. BASELINE_EQ: all spans in a line have the same baseline
|
|
82
|
-
if (line.spans.length > 1) {
|
|
83
|
-
const firstBaseline = line.baseline;
|
|
84
|
-
for (let j = 0; j < line.spans.length; j++) {
|
|
85
|
-
const span = line.spans[j];
|
|
86
|
-
const spanBaseline = span.fontMetrics.ascent;
|
|
87
|
-
if (Math.abs(spanBaseline - firstBaseline) > EPSILON) {
|
|
88
|
-
// NOTE: baseline may differ for super/sub — that's normal
|
|
89
|
-
// So we only check that baseline is set
|
|
90
|
-
if (spanBaseline <= 0) {
|
|
91
|
-
errors.push({
|
|
92
|
-
invariant: 'BASELINE_EQ',
|
|
93
|
-
message: `Line ${i}, span ${j}: baseline=${spanBaseline} is invalid`,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 3. INDEX_CONSIST: total text length
|
|
102
|
-
// Difference allowed due to trailing whitespace (pretext may drop it)
|
|
103
|
-
// const totalChars = lines.reduce((sum, l) => sum + (l.endIndex - l.startIndex), 0);
|
|
104
|
-
// if (totalChars > originalText.length + EPSILON) {
|
|
105
|
-
// errors.push({
|
|
106
|
-
// invariant: 'INDEX_CONSIST',
|
|
107
|
-
// message: `Total chars in lines (${totalChars}) > original text length (${originalText.length})`,
|
|
108
|
-
// details: { totalChars, originalLength: originalText.length },
|
|
109
|
-
// });
|
|
110
|
-
// }
|
|
111
|
-
|
|
112
|
-
// if (errors.length > 0) {
|
|
113
|
-
// const msg = errors.map(e => `[${e.invariant}] ${e.message}`).join('\n');
|
|
114
|
-
// throw new Error(`LineBox invariants violated:\n${msg}`);
|
|
115
|
-
// }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── YAML serialization ─────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
/** Span style label for snapshot */
|
|
121
|
-
function spanStyleLabel(span: Span): 'bold' | 'italic' | 'normal' {
|
|
122
|
-
if (span.style.fontStyle === 'italic') return 'italic';
|
|
123
|
-
const w = span.style.fontWeight;
|
|
124
|
-
if (w === 'bold' || w === 700) return 'bold';
|
|
125
|
-
return 'normal';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Convert Line[] to YAML string for snapshots.
|
|
130
|
-
* Only semantic data: text, x, width, style.
|
|
131
|
-
* No glyphAdvances, fontMetrics (noise), inlineWidget.
|
|
132
|
-
*/
|
|
133
|
-
export function linesToYAML(
|
|
134
|
-
lines: Line[],
|
|
135
|
-
paragraphWidth: number,
|
|
136
|
-
paragraphHeight: number,
|
|
137
|
-
): string {
|
|
138
|
-
const obj: SemanticParagraph = {
|
|
139
|
-
width: paragraphWidth,
|
|
140
|
-
height: paragraphHeight,
|
|
141
|
-
lines: lines.map(line => ({
|
|
142
|
-
y: Math.round(line.y * 100) / 100,
|
|
143
|
-
width: Math.round(line.width * 100) / 100,
|
|
144
|
-
height: Math.round(line.height * 100) / 100,
|
|
145
|
-
baseline: Math.round(line.baseline * 100) / 100,
|
|
146
|
-
fragments: line.spans.map(span => ({
|
|
147
|
-
text: span.text,
|
|
148
|
-
x: Math.round(span.x * 100) / 100,
|
|
149
|
-
width: Math.round(span.width * 100) / 100,
|
|
150
|
-
...(spanStyleLabel(span) !== 'normal' ? { style: spanStyleLabel(span) } : {}),
|
|
151
|
-
})),
|
|
152
|
-
})),
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
return dump(obj, {
|
|
156
|
-
indent: 2,
|
|
157
|
-
lineWidth: 120,
|
|
158
|
-
noRefs: true,
|
|
159
|
-
sortKeys: false,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
@@ -1,202 +0,0 @@
|
|
|
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
|
-
|
|
16
|
-
// Polyfill OffscreenCanvas for Node.js (node-canvas)
|
|
17
|
-
import '../measure/canvas-polyfill.js';
|
|
18
|
-
|
|
19
|
-
import type { Paragraph } from '../types/Document.js';
|
|
20
|
-
import type { FontMetrics } from '../types/FontTypes.js';
|
|
21
|
-
import type { IFontMetricsProvider } from '../types/FontTypes.js';
|
|
22
|
-
import type { ParagraphLayoutResult } from '../types/LayoutTypes.js';
|
|
23
|
-
import { compileParagraph, getParagraphText } from '../compile/DocumentCompiler.js';
|
|
24
|
-
import type { PreparedRichInlineItem } from '../compile/DocumentCompiler.js';
|
|
25
|
-
import { fontMetricsProvider } from '../measure/FontMetricsProvider.js';
|
|
26
|
-
import { positionLines } from './PositioningEngine.js';
|
|
27
|
-
import { assertLineInvariants } from './LineBoxValidator.js';
|
|
28
|
-
|
|
29
|
-
// @ts-ignore
|
|
30
|
-
import { prepareRichInline, materializeRichInlineLineRange, walkRichInlineLineRanges, type PreparedRichInline } from '@chenglou/pretext/rich-inline';
|
|
31
|
-
|
|
32
|
-
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
/** Get FontMetrics for a PreparedRichInlineItem */
|
|
35
|
-
function getFontMetricsForItem(item: PreparedRichInlineItem): FontMetrics {
|
|
36
|
-
return fontMetricsProvider.getMetrics(
|
|
37
|
-
item.metadata.style.fontFamily,
|
|
38
|
-
item.metadata.effectiveFontSize,
|
|
39
|
-
String(item.metadata.style.fontWeight || 400),
|
|
40
|
-
item.metadata.style.fontStyle || 'normal',
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── ParagraphLayoutEngine ─────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
export class ParagraphLayoutEngine {
|
|
47
|
-
private preparedCache = new Map<string, PreparedRichInline>();
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Layout a single paragraph — basic variant, no per-glyph data.
|
|
51
|
-
*
|
|
52
|
-
* @param paragraph — input paragraph
|
|
53
|
-
* @param maxWidth — available container width (px)
|
|
54
|
-
* @param fontProvider — optional metrics provider (default: fontMetricsProvider)
|
|
55
|
-
* @returns ParagraphLayoutResult with Line[]
|
|
56
|
-
*/
|
|
57
|
-
layout(
|
|
58
|
-
paragraph: Paragraph,
|
|
59
|
-
maxWidth: number,
|
|
60
|
-
yOffset: number = 0,
|
|
61
|
-
fontProvider?: IFontMetricsProvider,
|
|
62
|
-
): ParagraphLayoutResult {
|
|
63
|
-
const provider = fontProvider || fontMetricsProvider;
|
|
64
|
-
|
|
65
|
-
// Phase 1: Compile
|
|
66
|
-
const items = compileParagraph(paragraph);
|
|
67
|
-
|
|
68
|
-
// Phase 2: Prepare (cached)
|
|
69
|
-
const cacheKey = JSON.stringify(paragraph);
|
|
70
|
-
let prepared = this.preparedCache.get(cacheKey);
|
|
71
|
-
if (!prepared) {
|
|
72
|
-
prepared = prepareRichInline(items);
|
|
73
|
-
this.preparedCache.set(cacheKey, prepared);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Phase 3: Layout — walk lines
|
|
77
|
-
// CSS white-space: nowrap → disable wrapping (infinite width)
|
|
78
|
-
const effectiveMaxWidth = paragraph.style.whiteSpace === 'nowrap' ? Infinity : maxWidth;
|
|
79
|
-
const pretextLines: any[] = [];
|
|
80
|
-
walkRichInlineLineRanges(prepared, effectiveMaxWidth, (range: any) => {
|
|
81
|
-
pretextLines.push(range);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Materialize each line
|
|
85
|
-
const materializedLines: any[] = [];
|
|
86
|
-
for (const range of pretextLines) {
|
|
87
|
-
materializedLines.push(materializeRichInlineLineRange(prepared, range));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Phase 4: Position
|
|
91
|
-
const renderMode = provider.getMode();
|
|
92
|
-
const { lines, contentWidth } = positionLines(
|
|
93
|
-
materializedLines,
|
|
94
|
-
items,
|
|
95
|
-
(item) => {
|
|
96
|
-
if (fontProvider) {
|
|
97
|
-
return fontProvider.getMetrics(
|
|
98
|
-
item.metadata.style.fontFamily,
|
|
99
|
-
item.metadata.effectiveFontSize,
|
|
100
|
-
String(item.metadata.style.fontWeight || 400),
|
|
101
|
-
item.metadata.style.fontStyle || 'normal',
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
return getFontMetricsForItem(item);
|
|
105
|
-
},
|
|
106
|
-
paragraph.style,
|
|
107
|
-
maxWidth,
|
|
108
|
-
yOffset,
|
|
109
|
-
renderMode,
|
|
110
|
-
paragraph.id,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
// Phase 5: Validate
|
|
114
|
-
assertLineInvariants(lines, getParagraphText(paragraph), maxWidth);
|
|
115
|
-
|
|
116
|
-
// Phase 6: Compute height
|
|
117
|
-
const totalHeight = lines.length > 0
|
|
118
|
-
? lines[lines.length - 1].y + lines[lines.length - 1].height + (paragraph.style.spaceAfter || 0)
|
|
119
|
-
: 0;
|
|
120
|
-
|
|
121
|
-
// Phase 7: Content BBox
|
|
122
|
-
const contentHeight = lines.length > 0
|
|
123
|
-
? lines[lines.length - 1].y + lines[lines.length - 1].height
|
|
124
|
-
: 0;
|
|
125
|
-
|
|
126
|
-
return { width: maxWidth, height: totalHeight, lines, contentWidth, contentHeight };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Layout with per-glyph advance widths (for SVG glyph mode).
|
|
131
|
-
*
|
|
132
|
-
* After basic layout, fills Span.glyphAdvances
|
|
133
|
-
* via fontkit for each text span.
|
|
134
|
-
*
|
|
135
|
-
* @param paragraph — input paragraph
|
|
136
|
-
* @param maxWidth — available container width (px)
|
|
137
|
-
* @param yOffset — starting Y position
|
|
138
|
-
* @returns ParagraphLayoutResult with glyphAdvances[]
|
|
139
|
-
*/
|
|
140
|
-
layoutGlyph(
|
|
141
|
-
paragraph: Paragraph,
|
|
142
|
-
maxWidth: number,
|
|
143
|
-
yOffset: number = 0,
|
|
144
|
-
): ParagraphLayoutResult {
|
|
145
|
-
const result = this.layout(paragraph, maxWidth, yOffset);
|
|
146
|
-
|
|
147
|
-
for (const line of result.lines) {
|
|
148
|
-
for (const span of line.spans) {
|
|
149
|
-
if (span.type === 'text' && span.text.length > 0) {
|
|
150
|
-
span.glyphAdvances = this.computeGlyphAdvances(
|
|
151
|
-
span.text,
|
|
152
|
-
span.style.fontFamily,
|
|
153
|
-
span.fontMetrics.fontSize,
|
|
154
|
-
String(span.style.fontWeight || 400),
|
|
155
|
-
span.style.fontStyle || 'normal',
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Compute per-character advance widths via fontkit.
|
|
166
|
-
*/
|
|
167
|
-
private computeGlyphAdvances(
|
|
168
|
-
text: string,
|
|
169
|
-
fontFamily: string,
|
|
170
|
-
fontSize: number,
|
|
171
|
-
weight: string,
|
|
172
|
-
style: string,
|
|
173
|
-
): number[] {
|
|
174
|
-
const font = fontMetricsProvider.getFont(fontFamily, weight, style);
|
|
175
|
-
if (!font) {
|
|
176
|
-
// Fallback: uniform distribution
|
|
177
|
-
const avgWidth = fontSize * 0.6;
|
|
178
|
-
return Array.from(text).map(() => avgWidth);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const scale = fontSize / font.unitsPerEm;
|
|
182
|
-
const advances: number[] = [];
|
|
183
|
-
|
|
184
|
-
for (let i = 0; i < text.length; i++) {
|
|
185
|
-
const codePoint = text.codePointAt(i)!;
|
|
186
|
-
const glyph = font.glyphForCodePoint(codePoint);
|
|
187
|
-
if (glyph) {
|
|
188
|
-
advances.push(glyph.advanceWidth * scale);
|
|
189
|
-
} else {
|
|
190
|
-
// Missing glyph
|
|
191
|
-
advances.push(fontSize * 0.5);
|
|
192
|
-
}
|
|
193
|
-
// Skip surrogate pair
|
|
194
|
-
if (codePoint > 0xffff) i++;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return advances;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Singleton */
|
|
202
|
-
export const paragraphLayoutEngine = new ParagraphLayoutEngine();
|