@vyaz/core 0.0.1 → 0.0.3
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 +147950 -0
- package/package.json +7 -3
- package/src/compile/DocumentCompiler.ts +51 -9
- package/src/index.ts +9 -5
- package/src/layout/LineBoxValidator.ts +26 -25
- package/src/layout/ParagraphLayoutEngine.ts +17 -17
- package/src/layout/PositioningEngine.ts +56 -46
- package/src/layout/TextFrameLayoutEngine.ts +11 -6
- package/src/measure/FontMetricsProvider.ts +52 -16
- package/src/measure/FontNotFoundError.ts +16 -0
- package/src/measure/SystemFontRegistry.ts +141 -0
- package/src/measure/canvas-polyfill.ts +31 -3
- package/src/types/FontTypes.ts +3 -0
- package/src/types/LayoutTypes.ts +26 -21
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* TextFrameLayoutEngine.ts — Layout a full TextFrame (multi-paragraph).
|
|
3
3
|
*
|
|
4
4
|
* Pipeline:
|
|
5
|
-
* TextFrame → Paragraph[] → paragraphLayoutEngine.layout() each → merge
|
|
5
|
+
* TextFrame → Paragraph[] → paragraphLayoutEngine.layout() each → merge Line[]
|
|
6
6
|
*
|
|
7
7
|
* Handles:
|
|
8
8
|
* - Paragraph stacking with Y offset accumulation
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - frame.width/height optional → fitHorizontal/fitVertical flags
|
|
11
11
|
*/
|
|
12
12
|
import type { TextFrame } from '../types/Document.js';
|
|
13
|
-
import type {
|
|
13
|
+
import type { Line } from '../types/LayoutTypes.js';
|
|
14
14
|
import { paragraphLayoutEngine } from './ParagraphLayoutEngine.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -21,7 +21,7 @@ import { paragraphLayoutEngine } from './ParagraphLayoutEngine.js';
|
|
|
21
21
|
* - `'content'` → use `contentWidth` / `contentHeight`
|
|
22
22
|
*/
|
|
23
23
|
export interface TextFrameLayoutResult {
|
|
24
|
-
lines:
|
|
24
|
+
lines: Line[];
|
|
25
25
|
/** Frame width (set when TextFrame.width was provided). */
|
|
26
26
|
frameWidth?: number;
|
|
27
27
|
/** Frame height (set when TextFrame.height was provided). */
|
|
@@ -40,8 +40,8 @@ export interface TextFrameLayoutResult {
|
|
|
40
40
|
* Layout a full TextFrame by stacking paragraphs with Y offset accumulation.
|
|
41
41
|
*/
|
|
42
42
|
export function layoutTextFrame(frame: TextFrame): TextFrameLayoutResult {
|
|
43
|
-
const allLines:
|
|
44
|
-
let yOffset = 0;
|
|
43
|
+
const allLines: Line[] = [];
|
|
44
|
+
let yOffset = frame.padding?.top ?? 0;
|
|
45
45
|
let contentWidth = 0;
|
|
46
46
|
|
|
47
47
|
const leftPad = frame.padding?.left ?? 0;
|
|
@@ -56,6 +56,11 @@ export function layoutTextFrame(frame: TextFrame): TextFrameLayoutResult {
|
|
|
56
56
|
? frame.width - leftPad - rightPad
|
|
57
57
|
: Infinity;
|
|
58
58
|
|
|
59
|
+
// If wrap is disabled, force no-wrap on the paragraph
|
|
60
|
+
if (frame.wrap === false) {
|
|
61
|
+
p.style = { ...p.style, whiteSpace: 'nowrap' };
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
const result = paragraphLayoutEngine.layout(p, maxWidth, yOffset);
|
|
60
65
|
|
|
61
66
|
for (const line of result.lines) {
|
|
@@ -83,4 +88,4 @@ export function layoutTextFrame(frame: TextFrame): TextFrameLayoutResult {
|
|
|
83
88
|
fitHorizontal: frame.width !== undefined ? 'frame' : 'content',
|
|
84
89
|
fitVertical: frame.height !== undefined ? 'frame' : 'content',
|
|
85
90
|
};
|
|
86
|
-
}
|
|
91
|
+
}
|
|
@@ -12,11 +12,43 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { FontMetrics, IFontMetricsProvider } from '../types/FontTypes.js';
|
|
15
|
-
import { enableOfficeTextMeasure, disableOfficeTextMeasure } from './canvas-polyfill.js';
|
|
15
|
+
import { enableOfficeTextMeasure, disableOfficeTextMeasure, registerCanvasFont } from './canvas-polyfill.js';
|
|
16
|
+
import { FontNotFoundError } from './FontNotFoundError.js';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
// ── Weight normalisation ─────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Map named font weights to numeric strings.
|
|
22
|
+
* Both directions work: 'bold' → '700', 700 → '700', '700' → '700'.
|
|
23
|
+
*/
|
|
24
|
+
const WEIGHT_TO_NUM: Record<string, string> = {
|
|
25
|
+
thin: '100',
|
|
26
|
+
hairline: '100',
|
|
27
|
+
ultralight: '200',
|
|
28
|
+
extralight: '200',
|
|
29
|
+
light: '300',
|
|
30
|
+
normal: '400',
|
|
31
|
+
medium: '500',
|
|
32
|
+
semibold: '600',
|
|
33
|
+
demibold: '600',
|
|
34
|
+
bold: '700',
|
|
35
|
+
ultrabold: '800',
|
|
36
|
+
extrabold: '800',
|
|
37
|
+
heavy: '900',
|
|
38
|
+
black: '900',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Normalise weight to a numeric string ("400", "700", etc.). */
|
|
42
|
+
function normaliseWeight(weight: string): string {
|
|
43
|
+
const lower = weight.toLowerCase();
|
|
44
|
+
const mapped = WEIGHT_TO_NUM[lower];
|
|
45
|
+
if (mapped) return mapped;
|
|
46
|
+
return weight; // already numeric or unrecognised — pass through
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Cache key: "${family}_${weight}_${style}" with normalised weight. */
|
|
18
50
|
function cacheKey(family: string, weight: string, style: string): string {
|
|
19
|
-
return `${family}_${weight}_${style}`;
|
|
51
|
+
return `${family}_${normaliseWeight(weight)}_${style}`;
|
|
20
52
|
}
|
|
21
53
|
|
|
22
54
|
export class FontMetricsProvider implements IFontMetricsProvider {
|
|
@@ -48,12 +80,15 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
48
80
|
|
|
49
81
|
/**
|
|
50
82
|
* Register a binary font for use with fontkit.
|
|
51
|
-
* In browser — no-op.
|
|
83
|
+
* In browser — no-op (fonts are registered via CSS @font-face).
|
|
84
|
+
*
|
|
85
|
+
* @param sourcePath — if provided, also registers with @napi-rs/canvas for Node.js canvas measureText
|
|
52
86
|
*/
|
|
53
87
|
async registerFont(
|
|
54
88
|
family: string,
|
|
55
89
|
options: { weight?: string; style?: string },
|
|
56
90
|
source: string | Buffer,
|
|
91
|
+
sourcePath?: string,
|
|
57
92
|
): Promise<void> {
|
|
58
93
|
try {
|
|
59
94
|
// Dynamic ESM import — fontkit may not be available in browser
|
|
@@ -70,6 +105,11 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
70
105
|
this.cache.set(key, font);
|
|
71
106
|
// Invalidate metrics for this font
|
|
72
107
|
this.metricsCache.delete(key);
|
|
108
|
+
|
|
109
|
+
// Also register with @napi-rs/canvas so ctx.measureText() uses real fonts
|
|
110
|
+
if (sourcePath) {
|
|
111
|
+
registerCanvasFont(sourcePath, family);
|
|
112
|
+
}
|
|
73
113
|
} catch {
|
|
74
114
|
// fontkit not available (browser) — no-op
|
|
75
115
|
}
|
|
@@ -150,7 +190,12 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
150
190
|
return metrics;
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
// Strategy 2:
|
|
193
|
+
// Strategy 2: If fontkit cache is non-empty, fontkit is available but font not found → throw
|
|
194
|
+
if (this.cache.size > 0) {
|
|
195
|
+
throw new FontNotFoundError(fontFamily, weight, style);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Strategy 3: Canvas TextMetrics (browser, fallback when fontkit unavailable)
|
|
154
199
|
if (typeof document !== 'undefined') {
|
|
155
200
|
try {
|
|
156
201
|
const canvas = document.createElement('canvas');
|
|
@@ -172,17 +217,8 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
172
217
|
}
|
|
173
218
|
}
|
|
174
219
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
metrics = {
|
|
178
|
-
ascent: fontSize * 0.85,
|
|
179
|
-
descent: fontSize * 0.15,
|
|
180
|
-
capHeight: fontSize * 0.7,
|
|
181
|
-
unitsPerEm: 1000,
|
|
182
|
-
sourceTable: 'fallback',
|
|
183
|
-
};
|
|
184
|
-
this.metricsCache.set(metricsKey, metrics);
|
|
185
|
-
return metrics;
|
|
220
|
+
// Font not found — throw error with clear message
|
|
221
|
+
throw new FontNotFoundError(fontFamily, weight, style);
|
|
186
222
|
}
|
|
187
223
|
}
|
|
188
224
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FontNotFoundError.ts — thrown when a requested font is not registered.
|
|
3
|
+
*/
|
|
4
|
+
export class FontNotFoundError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
family: string,
|
|
7
|
+
weight = 'normal',
|
|
8
|
+
style = 'normal',
|
|
9
|
+
) {
|
|
10
|
+
super(
|
|
11
|
+
`Font not found: "${family}" (weight: ${weight}, style: ${style}). ` +
|
|
12
|
+
`Use SystemFontRegistry.scan() to register system fonts.`,
|
|
13
|
+
);
|
|
14
|
+
this.name = 'FontNotFoundError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import getSystemFonts from 'get-system-fonts';
|
|
13
|
+
import { fontMetricsProvider } from './FontMetricsProvider.js';
|
|
14
|
+
|
|
15
|
+
interface ScanResult {
|
|
16
|
+
/** Total font files found on the system */
|
|
17
|
+
total: number;
|
|
18
|
+
/** Number of successfully registered font files */
|
|
19
|
+
registered: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse font subfamily name into weight/style.
|
|
24
|
+
* Examples:
|
|
25
|
+
* "Regular" → { weight: 'normal', style: 'normal' }
|
|
26
|
+
* "Bold" → { weight: 'bold', style: 'normal' }
|
|
27
|
+
* "Italic" → { weight: 'normal', style: 'italic' }
|
|
28
|
+
* "Bold Italic" → { weight: 'bold', style: 'italic' }
|
|
29
|
+
* "Light" → { weight: 'light', style: 'normal' }
|
|
30
|
+
* "Medium" → { weight: 'medium', style: 'normal' }
|
|
31
|
+
* "Semi Bold" → { weight: 'semibold', style: 'normal' }
|
|
32
|
+
* "Black" → { weight: 'black', style: 'normal' }
|
|
33
|
+
*/
|
|
34
|
+
function parseSubfamily(subfamily: string): { weight: string; style: string } {
|
|
35
|
+
const lower = subfamily.toLowerCase();
|
|
36
|
+
|
|
37
|
+
let style: string;
|
|
38
|
+
if (lower.includes('italic')) {
|
|
39
|
+
style = 'italic';
|
|
40
|
+
} else {
|
|
41
|
+
style = 'normal';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let weight: string;
|
|
45
|
+
if (lower.includes('thin') || lower.includes('hairline')) {
|
|
46
|
+
weight = 'thin';
|
|
47
|
+
} else if (lower.includes('extralight') || lower.includes('ultralight')) {
|
|
48
|
+
weight = 'extralight';
|
|
49
|
+
} else if (lower.includes('light')) {
|
|
50
|
+
weight = 'light';
|
|
51
|
+
} else if (lower.includes('semibold') || lower.includes('demibold')) {
|
|
52
|
+
weight = 'semibold';
|
|
53
|
+
} else if (lower.includes('bold') || lower.includes('heavy') || lower.includes('black')) {
|
|
54
|
+
weight = 'bold';
|
|
55
|
+
} else if (lower.includes('medium') || lower.includes('medium')) {
|
|
56
|
+
weight = 'medium';
|
|
57
|
+
} else {
|
|
58
|
+
weight = 'normal';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { weight, style };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class SystemFontRegistry {
|
|
65
|
+
private static _instance: SystemFontRegistry;
|
|
66
|
+
|
|
67
|
+
/** Map of registered family names → true */
|
|
68
|
+
private registered = new Map<string, true>();
|
|
69
|
+
|
|
70
|
+
private constructor() {}
|
|
71
|
+
|
|
72
|
+
static get instance(): SystemFontRegistry {
|
|
73
|
+
if (!SystemFontRegistry._instance) {
|
|
74
|
+
SystemFontRegistry._instance = new SystemFontRegistry();
|
|
75
|
+
}
|
|
76
|
+
return SystemFontRegistry._instance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Scan the system for all fonts and register them in FontMetricsProvider.
|
|
81
|
+
*
|
|
82
|
+
* - Uses `get-system-fonts` to find all .ttf/.otf files
|
|
83
|
+
* - Opens each with fontkit to extract familyName/subfamilyName
|
|
84
|
+
* - Registers in fontMetricsProvider (fontkit buffer + canvas path)
|
|
85
|
+
*
|
|
86
|
+
* @returns stats about what was found and registered
|
|
87
|
+
*/
|
|
88
|
+
async scan(): Promise<ScanResult> {
|
|
89
|
+
const paths: string[] = await getSystemFonts();
|
|
90
|
+
let registered = 0;
|
|
91
|
+
|
|
92
|
+
for (const fontPath of paths) {
|
|
93
|
+
try {
|
|
94
|
+
const buffer = readFileSync(fontPath);
|
|
95
|
+
|
|
96
|
+
// Dynamic import — fontkit may not be available in browser
|
|
97
|
+
let fontkit: any;
|
|
98
|
+
try {
|
|
99
|
+
fontkit = await import('fontkit');
|
|
100
|
+
// @ts-ignore fontkit CJS/ESM compatibility
|
|
101
|
+
fontkit = fontkit.default || fontkit;
|
|
102
|
+
} catch {
|
|
103
|
+
// fontkit not available — skip font registration
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const font = fontkit.create(buffer);
|
|
108
|
+
const family = font.familyName;
|
|
109
|
+
if (!family) continue;
|
|
110
|
+
|
|
111
|
+
const { weight, style } = parseSubfamily(font.subfamilyName || 'Regular');
|
|
112
|
+
|
|
113
|
+
await fontMetricsProvider.registerFont(family, { weight, style }, buffer, fontPath);
|
|
114
|
+
this.registered.set(family, true);
|
|
115
|
+
registered++;
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip unreadable / invalid font files
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { total: paths.length, registered };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a font family is registered.
|
|
127
|
+
*/
|
|
128
|
+
isRegistered(family: string): boolean {
|
|
129
|
+
return this.registered.has(family);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get list of all registered font families.
|
|
134
|
+
*/
|
|
135
|
+
getRegisteredFamilies(): string[] {
|
|
136
|
+
return Array.from(this.registered.keys());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Singleton instance */
|
|
141
|
+
export const systemFontRegistry = SystemFontRegistry.instance;
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
* Exports enableOfficeTextMeasure / disableOfficeTextMeasure —
|
|
11
11
|
* ctx.measureText override for fontkit-based measurements in Office mode.
|
|
12
12
|
*/
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
|
|
13
15
|
// ⚠️ Dynamic import — prevents esbuild from resolving @napi-rs/canvas at bundle time.
|
|
14
16
|
// This module is a native Node.js addon. In the browser, Canvas APIs are already available.
|
|
15
17
|
let _createCanvas: ((w: number, h: number) => any) | null = null;
|
|
16
18
|
|
|
19
|
+
const _require = createRequire(import.meta.url);
|
|
17
20
|
try {
|
|
18
|
-
|
|
19
|
-
const mod: any = (Function('return require("@napi-rs/canvas")'))();
|
|
21
|
+
const mod: any = _require('@napi-rs/canvas');
|
|
20
22
|
_createCanvas = mod.createCanvas;
|
|
21
23
|
} catch {
|
|
22
24
|
// Browser — document.createElement('canvas') is available natively, no polyfill needed
|
|
@@ -28,7 +30,18 @@ try {
|
|
|
28
30
|
// to create a temporary Canvas and obtain a 2d context for measureText.
|
|
29
31
|
// Without this polyfill, prepare() crashes with "document is not defined" in Bun.
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
function needsCanvasPolyfill(): boolean {
|
|
34
|
+
if (typeof globalThis.document === 'undefined') return true;
|
|
35
|
+
try {
|
|
36
|
+
const el = globalThis.document.createElement('canvas');
|
|
37
|
+
if (!el || typeof el.getContext !== 'function') return true;
|
|
38
|
+
return false;
|
|
39
|
+
} catch {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (needsCanvasPolyfill()) {
|
|
32
45
|
(globalThis as any).document = {
|
|
33
46
|
createElement: (tag: string) => {
|
|
34
47
|
if (tag === 'canvas' && _createCanvas) return _createCanvas(1, 1);
|
|
@@ -156,6 +169,21 @@ function createEmptyMetrics(): TextMetrics {
|
|
|
156
169
|
return createMetricsObject(0);
|
|
157
170
|
}
|
|
158
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Register a font with @napi-rs/canvas so ctx.measureText() works in Node.js.
|
|
174
|
+
* No-op in browser or when @napi-rs/canvas is not available.
|
|
175
|
+
*/
|
|
176
|
+
export function registerCanvasFont(fontPath: string, family: string): void {
|
|
177
|
+
try {
|
|
178
|
+
const mod = _require('@napi-rs/canvas');
|
|
179
|
+
if (mod?.registerFont) {
|
|
180
|
+
mod.registerFont(fontPath, { family });
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// @napi-rs/canvas not available — no-op
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
/**
|
|
160
188
|
* Enable Office measurement: replaces ctx.measureText with fontkit-based version.
|
|
161
189
|
* @param fontCache — Map key->font from FontMetricsProvider
|
package/src/types/FontTypes.ts
CHANGED
|
@@ -45,11 +45,14 @@ export interface IFontMetricsProvider {
|
|
|
45
45
|
/**
|
|
46
46
|
* Register a binary font for use with fontkit.
|
|
47
47
|
* In browser — no-op (fonts are registered via CSS @font-face).
|
|
48
|
+
*
|
|
49
|
+
* @param sourcePath — path to .ttf/.otf file for optional @napi-rs/canvas.registerFont()
|
|
48
50
|
*/
|
|
49
51
|
registerFont(
|
|
50
52
|
family: string,
|
|
51
53
|
options: { weight?: string; style?: string },
|
|
52
54
|
source: string | Buffer,
|
|
55
|
+
sourcePath?: string,
|
|
53
56
|
): void;
|
|
54
57
|
|
|
55
58
|
/**
|
package/src/types/LayoutTypes.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LayoutTypes.ts — output types (Physical Box Model).
|
|
3
3
|
*
|
|
4
|
-
* ParagraphLayoutResult →
|
|
4
|
+
* ParagraphLayoutResult → Line[] → Span[]
|
|
5
5
|
* This is the contract between layout engine and renderers.
|
|
6
6
|
*
|
|
7
7
|
* Based on plan.md §2.5 (Output — Physical Box Model / Layout Tree)
|
|
@@ -9,22 +9,22 @@
|
|
|
9
9
|
|
|
10
10
|
import type { TextRun, InlineWidget, TextAlignment } from './Document.js';
|
|
11
11
|
|
|
12
|
-
// ──
|
|
12
|
+
// ── Span (render atom, formerly FragmentBox) ─────────────────────────────
|
|
13
13
|
|
|
14
|
-
export interface
|
|
15
|
-
/** Offset from
|
|
14
|
+
export interface Span {
|
|
15
|
+
/** Offset from Line.x */
|
|
16
16
|
x: number;
|
|
17
|
-
/** Physical
|
|
17
|
+
/** Physical span width */
|
|
18
18
|
width: number;
|
|
19
|
-
/**
|
|
19
|
+
/** Span text (or " " for justify spaces) */
|
|
20
20
|
text: string;
|
|
21
21
|
/** Index of the source run in the paragraph's `children` array. */
|
|
22
22
|
itemIndex: number;
|
|
23
23
|
/** ID of the source paragraph (for SVG grouping). */
|
|
24
24
|
paragraphId?: string;
|
|
25
25
|
|
|
26
|
-
/** Physical font metrics for this
|
|
27
|
-
fontMetrics:
|
|
26
|
+
/** Physical font metrics for this span */
|
|
27
|
+
fontMetrics: SpanFontMetrics;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* A snapshot of the source run's style at layout time.
|
|
@@ -32,20 +32,20 @@ export interface FragmentBox {
|
|
|
32
32
|
*/
|
|
33
33
|
style: TextRun;
|
|
34
34
|
|
|
35
|
-
/** InlineWidget data (if
|
|
35
|
+
/** InlineWidget data (if span is an inline-box) */
|
|
36
36
|
inlineWidget?: InlineWidget;
|
|
37
37
|
|
|
38
38
|
/** Per-character advance widths (for selection/tracking) */
|
|
39
39
|
glyphAdvances?: number[];
|
|
40
40
|
|
|
41
|
-
/**
|
|
41
|
+
/** Span type: 'text' — regular text, 'space' — whitespace span */
|
|
42
42
|
type: 'text' | 'space';
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Trailing whitespace flag.
|
|
46
|
-
* - true:
|
|
46
|
+
* - true: span is at end of line, does not participate in line advance
|
|
47
47
|
* and is not stretched during justify (zero width for calculations).
|
|
48
|
-
* - undefined/false: regular
|
|
48
|
+
* - undefined/false: regular span.
|
|
49
49
|
*
|
|
50
50
|
* See CSS Text Module Level 3 §4.1.3 (Tracking and Dropping Spaces)
|
|
51
51
|
* and Parley LineItemData::has_trailing_whitespace.
|
|
@@ -53,7 +53,7 @@ export interface FragmentBox {
|
|
|
53
53
|
trailing?: boolean;
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Line break mode after this
|
|
56
|
+
* Line break mode after this span.
|
|
57
57
|
* 'soft' — soft line break (insufficient space)
|
|
58
58
|
* 'hard' — forced break (\n, explicit separator)
|
|
59
59
|
* undefined — not end of line
|
|
@@ -61,22 +61,26 @@ export interface FragmentBox {
|
|
|
61
61
|
breakType?: 'soft' | 'hard';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export interface
|
|
64
|
+
export interface SpanFontMetrics {
|
|
65
65
|
ascent: number;
|
|
66
66
|
descent: number;
|
|
67
67
|
fontSize: number;
|
|
68
|
+
/** Vertical offset from baseline (px). Used for sub/superscript positioning.
|
|
69
|
+
* Negative = above baseline (superscript). Positive = below baseline (subscript).
|
|
70
|
+
* Undefined or 0 = normal baseline position. */
|
|
71
|
+
baselineOffset?: number;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
// ──
|
|
74
|
+
// ── Line (single line, formerly LineBox) ─────────────────────────────────
|
|
71
75
|
|
|
72
|
-
export interface
|
|
76
|
+
export interface Line {
|
|
73
77
|
/** Absolute X within container (alignment + indent) */
|
|
74
78
|
x: number;
|
|
75
79
|
/** Absolute Y of line top edge */
|
|
76
80
|
y: number;
|
|
77
81
|
/** Line content width (without alignment) */
|
|
78
82
|
width: number;
|
|
79
|
-
/** Full line height (max
|
|
83
|
+
/** Full line height (max spans × lineHeight) */
|
|
80
84
|
height: number;
|
|
81
85
|
|
|
82
86
|
/** Baseline offset from y */
|
|
@@ -94,7 +98,7 @@ export interface LineBox {
|
|
|
94
98
|
/** Paragraph alignment (optional, for PowerPoint render) */
|
|
95
99
|
alignment?: TextAlignment;
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
spans: Span[];
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
// ── ParagraphLayoutResult (single paragraph) ─────────────────────────────
|
|
@@ -102,7 +106,7 @@ export interface LineBox {
|
|
|
102
106
|
export interface ParagraphLayoutResult {
|
|
103
107
|
width: number; // paragraph width (maxWidth)
|
|
104
108
|
height: number; // full paragraph height including spacing
|
|
105
|
-
lines:
|
|
109
|
+
lines: Line[];
|
|
106
110
|
/** Actual content width (text bbox, without voids) */
|
|
107
111
|
contentWidth: number;
|
|
108
112
|
/** Actual content height (text bbox) */
|
|
@@ -111,7 +115,7 @@ export interface ParagraphLayoutResult {
|
|
|
111
115
|
|
|
112
116
|
// ── Text region for YAML snapshots ───────────────────────────────────────
|
|
113
117
|
|
|
114
|
-
/** Semantic
|
|
118
|
+
/** Semantic span for snapshots (without physical metrics) */
|
|
115
119
|
export interface SemanticFragment {
|
|
116
120
|
text: string;
|
|
117
121
|
x: number;
|
|
@@ -133,4 +137,5 @@ export interface SemanticParagraph {
|
|
|
133
137
|
width: number;
|
|
134
138
|
height: number;
|
|
135
139
|
lines: SemanticLine[];
|
|
136
|
-
}
|
|
140
|
+
}
|
|
141
|
+
|