@vyaz/core 0.0.4 → 0.0.5
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/package.json +22 -5
- package/src/compile/DocumentCompiler.ts +11 -3
- package/src/index.browser.ts +90 -0
- package/src/index.ts +17 -0
- package/src/layout/ParagraphLayoutEngine.ts +106 -44
- package/src/layout/PositioningEngine.ts +249 -35
- package/src/layout/TextFrameLayoutEngine.ts +293 -21
- package/src/layout/estimateWidth.ts +74 -0
- package/src/measure/FontEngine.ts +144 -0
- package/src/measure/FontMetricsProvider.ts +74 -76
- package/src/measure/SystemFontRegistry.ts +15 -14
- package/src/measure/canvas-polyfill.ts +17 -9
- package/src/types/Document.ts +129 -3
- package/src/types/LayoutTypes.ts +24 -7
- package/src/utils/env.ts +18 -0
- package/src/utils/font.ts +28 -0
- package/src/utils/groupLinesByParagraph.ts +74 -0
- package/src/utils/list.ts +107 -0
- package/src/utils/textTransform.ts +96 -0
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
* FontMetricsProvider.ts — isomorphic font metrics provider.
|
|
3
3
|
*
|
|
4
4
|
* Strategy (priority):
|
|
5
|
-
* 1.
|
|
6
|
-
* - 'browser' mode:
|
|
7
|
-
* - 'office' mode:
|
|
8
|
-
* 2. Canvas TextMetrics (browser)
|
|
9
|
-
* 3. Fallback (fontSize * 0.85 / 0.15)
|
|
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)
|
|
10
9
|
*
|
|
11
|
-
* Uses
|
|
10
|
+
* Uses FontEngine as the single entry point for all fontkit operations.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import type { FontMetrics, IFontMetricsProvider } from '../types/FontTypes.js';
|
|
15
|
-
import {
|
|
14
|
+
import type { FontFace } from './FontEngine.js';
|
|
15
|
+
import { enableOfficeTextMeasure, disableOfficeTextMeasure } from './canvas-polyfill.js';
|
|
16
16
|
import { FontNotFoundError } from './FontNotFoundError.js';
|
|
17
17
|
|
|
18
|
+
// ── Reasonable default for missing glyphs ──────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Factor used when a glyph is not found in the font.
|
|
22
|
+
* Multiplied by fontSize to estimate the missing glyph width.
|
|
23
|
+
* Used across all measurement code paths (ParagraphLayoutEngine, canvas-polyfill).
|
|
24
|
+
*/
|
|
25
|
+
export const MISSING_GLYPH_FACTOR = 0.5;
|
|
26
|
+
|
|
18
27
|
// ── Weight normalisation ─────────────────────────────────────────────────
|
|
19
28
|
|
|
20
29
|
/**
|
|
@@ -52,7 +61,8 @@ function cacheKey(family: string, weight: string, style: string): string {
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
export class FontMetricsProvider implements IFontMetricsProvider {
|
|
55
|
-
|
|
64
|
+
/** Map<string, FontFace> — font engine font face cache */
|
|
65
|
+
private cache = new Map<string, FontFace>();
|
|
56
66
|
private metricsCache = new Map<string, FontMetrics>();
|
|
57
67
|
private mode: 'browser' | 'office' = 'browser';
|
|
58
68
|
|
|
@@ -80,49 +90,52 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
80
90
|
|
|
81
91
|
/**
|
|
82
92
|
* Register a binary font for use with fontkit.
|
|
83
|
-
* In browser — no-op (fonts are registered via CSS @font-face).
|
|
84
93
|
*
|
|
85
|
-
*
|
|
94
|
+
* In both Node.js and browser the font is loaded via FontEngine.
|
|
95
|
+
* In the browser the caller must provide font bytes (e.g. fetched via
|
|
96
|
+
* `getFontBuffer()` from `../utils/font.js`).
|
|
97
|
+
*
|
|
98
|
+
* @param source Font file bytes (ArrayBuffer / Uint8Array), or a URL string
|
|
99
|
+
* @param sourcePath Optional filesystem path (used for @napi-rs/canvas in Node.js)
|
|
86
100
|
*/
|
|
87
101
|
async registerFont(
|
|
88
102
|
family: string,
|
|
89
103
|
options: { weight?: string; style?: string },
|
|
90
|
-
source: string |
|
|
104
|
+
source: string | ArrayBuffer | Uint8Array,
|
|
91
105
|
sourcePath?: string,
|
|
92
106
|
): Promise<void> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
const { createFontFace } = await import('./FontEngine.js');
|
|
108
|
+
const { registerCanvasFont } = await import('./canvas-polyfill.js');
|
|
109
|
+
|
|
110
|
+
// Convert string (URL) to buffer — works in browser via fetch
|
|
111
|
+
if (typeof source === 'string') {
|
|
112
|
+
const { getFontBuffer } = await import('../utils/font.js');
|
|
113
|
+
source = await getFontBuffer(source);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const font = await createFontFace(source);
|
|
117
|
+
const key = cacheKey(
|
|
118
|
+
family,
|
|
119
|
+
options.weight || 'normal',
|
|
120
|
+
options.style || 'normal',
|
|
121
|
+
);
|
|
122
|
+
this.cache.set(key, font);
|
|
123
|
+
// Invalidate metrics for this font
|
|
124
|
+
this.metricsCache.delete(key);
|
|
125
|
+
|
|
126
|
+
// Also register with @napi-rs/canvas so ctx.measureText() uses real fonts
|
|
127
|
+
if (sourcePath) {
|
|
128
|
+
registerCanvasFont(sourcePath, family);
|
|
115
129
|
}
|
|
116
|
-
return Promise.resolve();
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
// ── Font object access (for per-glyph advance) ────────────────────
|
|
120
133
|
|
|
121
134
|
/**
|
|
122
|
-
* Get
|
|
123
|
-
* Returns undefined if font is not registered
|
|
135
|
+
* Get font engine FontFace object for per-character calculations.
|
|
136
|
+
* Returns undefined if font is not registered.
|
|
124
137
|
*/
|
|
125
|
-
getFont(family: string, weight = 'normal', style = 'normal'):
|
|
138
|
+
getFont(family: string, weight = 'normal', style = 'normal'): FontFace | undefined {
|
|
126
139
|
const key = cacheKey(family, weight, style);
|
|
127
140
|
return this.cache.get(key);
|
|
128
141
|
}
|
|
@@ -142,55 +155,40 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
142
155
|
const cached = this.metricsCache.get(metricsKey);
|
|
143
156
|
if (cached) return cached;
|
|
144
157
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Strategy 1: fontkit
|
|
158
|
+
// Strategy 1: FontEngine (fontkit)
|
|
148
159
|
const font = this.cache.get(key);
|
|
149
160
|
|
|
150
161
|
if (font) {
|
|
162
|
+
// Inline computePixelMetrics to avoid circular ESM import
|
|
151
163
|
const scale = fontSize / font.unitsPerEm;
|
|
152
164
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
ascent = os2.winAscent * scale * 1.078;
|
|
162
|
-
descent = Math.abs(os2.winDescent) * scale * 1.078;
|
|
163
|
-
sourceTable = 'OS/2';
|
|
164
|
-
} else {
|
|
165
|
-
// Fallback to hhea if OS/2 is absent
|
|
166
|
-
ascent = font.ascent * scale;
|
|
167
|
-
descent = Math.abs(font.descent) * scale;
|
|
168
|
-
sourceTable = 'hhea';
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
metrics = {
|
|
172
|
-
ascent,
|
|
173
|
-
descent,
|
|
174
|
-
capHeight: (font.capHeight ?? ascent) * scale,
|
|
175
|
-
unitsPerEm: font.unitsPerEm,
|
|
176
|
-
sourceTable,
|
|
177
|
-
};
|
|
165
|
+
let ascent: number;
|
|
166
|
+
let descent: number;
|
|
167
|
+
let sourceTable: 'hhea' | 'OS/2';
|
|
168
|
+
|
|
169
|
+
if (this.mode === 'office' && font.winAscent != null && font.winDescent != null) {
|
|
170
|
+
ascent = font.winAscent * scale * 1.078;
|
|
171
|
+
descent = Math.abs(font.winDescent) * scale * 1.078;
|
|
172
|
+
sourceTable = 'OS/2';
|
|
178
173
|
} else {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
descent: Math.abs(font.descent) * scale,
|
|
183
|
-
capHeight: (font.capHeight ?? font.ascent) * scale,
|
|
184
|
-
unitsPerEm: font.unitsPerEm,
|
|
185
|
-
sourceTable: 'hhea',
|
|
186
|
-
};
|
|
174
|
+
ascent = font.ascent * scale;
|
|
175
|
+
descent = Math.abs(font.descent) * scale;
|
|
176
|
+
sourceTable = 'hhea';
|
|
187
177
|
}
|
|
188
178
|
|
|
179
|
+
const metrics: FontMetrics = {
|
|
180
|
+
ascent,
|
|
181
|
+
descent,
|
|
182
|
+
capHeight: (font.capHeight ?? font.ascent) * scale,
|
|
183
|
+
unitsPerEm: font.unitsPerEm,
|
|
184
|
+
sourceTable,
|
|
185
|
+
};
|
|
186
|
+
|
|
189
187
|
this.metricsCache.set(metricsKey, metrics);
|
|
190
188
|
return metrics;
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
// Strategy 2: If
|
|
191
|
+
// Strategy 2: If font cache is non-empty, fontkit is available but font not found → throw
|
|
194
192
|
if (this.cache.size > 0) {
|
|
195
193
|
throw new FontNotFoundError(fontFamily, weight, style);
|
|
196
194
|
}
|
|
@@ -203,7 +201,7 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
203
201
|
ctx.font = `${style} ${weight} ${fontSize}px ${fontFamily}`;
|
|
204
202
|
const m = ctx.measureText('M');
|
|
205
203
|
|
|
206
|
-
metrics = {
|
|
204
|
+
const metrics: FontMetrics = {
|
|
207
205
|
ascent: m.fontBoundingBoxAscent || fontSize * 0.85,
|
|
208
206
|
descent: m.fontBoundingBoxDescent || fontSize * 0.15,
|
|
209
207
|
capHeight: m.actualBoundingBoxAscent || fontSize * 0.7,
|
|
@@ -213,7 +211,7 @@ export class FontMetricsProvider implements IFontMetricsProvider {
|
|
|
213
211
|
this.metricsCache.set(metricsKey, metrics);
|
|
214
212
|
return metrics;
|
|
215
213
|
} catch {
|
|
216
|
-
// Fall through to
|
|
214
|
+
// Fall through to throw
|
|
217
215
|
}
|
|
218
216
|
}
|
|
219
217
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { fontMetricsProvider } from './FontMetricsProvider.js';
|
|
15
|
+
import { isNodeLike } from '../utils/env.js';
|
|
15
16
|
|
|
16
17
|
interface ScanResult {
|
|
17
18
|
/** Total font files found on the system */
|
|
@@ -87,6 +88,12 @@ export class SystemFontRegistry {
|
|
|
87
88
|
* @returns stats about what was found and registered
|
|
88
89
|
*/
|
|
89
90
|
async scan(): Promise<ScanResult> {
|
|
91
|
+
// System font scanning requires Node.js — no-op in browser
|
|
92
|
+
if (!isNodeLike) {
|
|
93
|
+
console.warn('[vyaz] systemFontRegistry.scan() недоступен в браузере');
|
|
94
|
+
return { total: 0, registered: 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
// Dynamic imports — hidden from bundler static analysis.
|
|
91
98
|
// Only resolves on Node.js. No-op in browser.
|
|
92
99
|
const [{ readFileSync }, getSystemFontsModule] = await Promise.all([
|
|
@@ -103,22 +110,16 @@ export class SystemFontRegistry {
|
|
|
103
110
|
try {
|
|
104
111
|
const buffer = readFileSync(fontPath);
|
|
105
112
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// fontkit not available — skip font registration
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const font = fontkit.create(buffer);
|
|
118
|
-
const family = font.familyName;
|
|
113
|
+
// Use FontEngine to extract family name — avoids direct fontkit import
|
|
114
|
+
const { createFontFace } = await import('./FontEngine.js');
|
|
115
|
+
const font = await createFontFace(buffer);
|
|
116
|
+
// Access raw fontkit object for familyName — FontFace doesn't expose it directly
|
|
117
|
+
// but we need it to know the family before calling registerFont
|
|
118
|
+
const fontkit = (font as any)._raw;
|
|
119
|
+
const family = fontkit.familyName;
|
|
119
120
|
if (!family) continue;
|
|
120
121
|
|
|
121
|
-
const { weight, style } = parseSubfamily(
|
|
122
|
+
const { weight, style } = parseSubfamily(fontkit.subfamilyName || 'Regular');
|
|
122
123
|
|
|
123
124
|
await fontMetricsProvider.registerFont(family, { weight, style }, buffer, fontPath);
|
|
124
125
|
this.registered.set(family, true);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canvas API polyfill for Bun/Node.js via @napi-rs/canvas.
|
|
3
|
+
*
|
|
3
4
|
* Required by @chenglou/pretext in server environments (Bun/Node.js without DOM).
|
|
4
5
|
*
|
|
5
6
|
* Two levels of polyfill:
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
* 2. OffscreenCanvas — for some libraries and early versions.
|
|
9
10
|
*
|
|
10
11
|
* Exports enableOfficeTextMeasure / disableOfficeTextMeasure —
|
|
11
|
-
* ctx.measureText override for
|
|
12
|
+
* ctx.measureText override for FontEngine-based measurements in Office mode.
|
|
12
13
|
*
|
|
13
14
|
* ⚠️ All Node.js built-in module imports are dynamic (lazy) to avoid
|
|
14
15
|
* Vite/Webpack externalization errors in browser builds.
|
|
@@ -110,7 +111,10 @@ const originalMeasureText = (globalThis as any).CanvasRenderingContext2D
|
|
|
110
111
|
| ((text: string) => TextMetrics)
|
|
111
112
|
| undefined;
|
|
112
113
|
|
|
113
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* fontCache: Map<family_weight_style, FontFace> where FontFace._raw holds the
|
|
116
|
+
* raw fontkit font object. Used by enableOfficeTextMeasure.
|
|
117
|
+
*/
|
|
114
118
|
let officeFontCache: Map<string, any> | null = null;
|
|
115
119
|
let officeEnabled = false;
|
|
116
120
|
|
|
@@ -146,17 +150,19 @@ function officeMeasureText(this: any, text: string): TextMetrics {
|
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
const key = cacheKey(parsed.family, parsed.weight);
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
153
|
+
const fontFace = officeFontCache.get(key);
|
|
154
|
+
if (!fontFace) {
|
|
151
155
|
return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
|
|
158
|
+
// FontFace._raw holds the raw fontkit font object
|
|
159
|
+
const raw: any = fontFace._raw;
|
|
160
|
+
const scale = parsed.size / fontFace.unitsPerEm;
|
|
155
161
|
let totalWidth = 0;
|
|
156
162
|
|
|
157
163
|
for (let i = 0; i < text.length; i++) {
|
|
158
164
|
const codePoint = text.codePointAt(i)!;
|
|
159
|
-
const glyph =
|
|
165
|
+
const glyph = raw.glyphForCodePoint(codePoint);
|
|
160
166
|
if (glyph) {
|
|
161
167
|
totalWidth += glyph.advanceWidth * scale;
|
|
162
168
|
} else {
|
|
@@ -164,7 +170,9 @@ function officeMeasureText(this: any, text: string): TextMetrics {
|
|
|
164
170
|
if (originalMeasureText) {
|
|
165
171
|
return originalMeasureText.call(this, text);
|
|
166
172
|
}
|
|
167
|
-
|
|
173
|
+
// Uses the same MISSING_GLYPH_FACTOR (0.5) as FontMetricsProvider
|
|
174
|
+
// to avoid circular dependency. Keep in sync.
|
|
175
|
+
totalWidth += parsed.size * 0.5;
|
|
168
176
|
}
|
|
169
177
|
// Skip surrogate pairs
|
|
170
178
|
if (codePoint > 0xffff) i++;
|
|
@@ -206,8 +214,8 @@ export function registerCanvasFont(fontPath: string, family: string): void {
|
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
/**
|
|
209
|
-
* Enable Office measurement: replaces ctx.measureText with
|
|
210
|
-
* @param fontCache — Map
|
|
217
|
+
* Enable Office measurement: replaces ctx.measureText with FontEngine-based version.
|
|
218
|
+
* @param fontCache — Map<family_weight_style, FontFace> from FontMetricsProvider
|
|
211
219
|
*/
|
|
212
220
|
export function enableOfficeTextMeasure(fontCache: Map<string, any>): void {
|
|
213
221
|
if (officeEnabled) return;
|
package/src/types/Document.ts
CHANGED
|
@@ -96,7 +96,6 @@ export type TextDecorationStyle = 'solid' | 'double' | 'dotted' | 'dashed' | 'wa
|
|
|
96
96
|
* Text case transform.
|
|
97
97
|
*
|
|
98
98
|
* @see {@link https://www.w3.org/TR/css-text-3/#text-transform-property | CSS Text: text-transform}
|
|
99
|
-
* @todo Not yet implemented in the layout engine.
|
|
100
99
|
*/
|
|
101
100
|
export type TextTransform = 'none' | 'uppercase' | 'lowercase' | 'capitalize';
|
|
102
101
|
|
|
@@ -143,6 +142,40 @@ export type ScriptType = 'normal' | 'sub' | 'super';
|
|
|
143
142
|
*/
|
|
144
143
|
export type WhiteSpace = 'normal' | 'nowrap' | 'pre';
|
|
145
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Type of list marker.
|
|
147
|
+
*
|
|
148
|
+
* - `'bullet'`: unordered list (disc, circle, square).
|
|
149
|
+
* - `'number'`: ordered list (decimal, roman, alpha).
|
|
150
|
+
* - `'none'`: no list marker.
|
|
151
|
+
*/
|
|
152
|
+
export type ListType = 'bullet' | 'number' | 'none';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Numbering format for ordered lists.
|
|
156
|
+
*
|
|
157
|
+
* - `'decimal'`: 1, 2, 3, ...
|
|
158
|
+
* - `'upper-roman'`: I, II, III, ...
|
|
159
|
+
* - `'lower-roman'`: i, ii, iii, ...
|
|
160
|
+
* - `'upper-alpha'`: A, B, C, ...
|
|
161
|
+
* - `'lower-alpha'`: a, b, c, ...
|
|
162
|
+
*
|
|
163
|
+
* @see {@link https://www.w3.org/TR/css-lists-3/#counter-format | CSS Lists: counter-format}
|
|
164
|
+
*/
|
|
165
|
+
export type NumberFormat = 'decimal' | 'upper-roman' | 'lower-roman' | 'upper-alpha' | 'lower-alpha';
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Position of the list marker relative to the text.
|
|
169
|
+
*
|
|
170
|
+
* - `'outside'`: marker hangs to the left of the text block (default). All lines
|
|
171
|
+
* share the same indent — the marker sits inside the indent zone.
|
|
172
|
+
* - `'inside'`: marker is the first inline element in the text flow, on the first
|
|
173
|
+
* line only.
|
|
174
|
+
*
|
|
175
|
+
* @see {@link https://www.w3.org/TR/css-lists-3/#list-style-position-property | CSS Lists: list-style-position}
|
|
176
|
+
*/
|
|
177
|
+
export type ListStylePosition = 'outside' | 'inside';
|
|
178
|
+
|
|
146
179
|
// ── Autofit ─────────────────────────────────────────────────────────────
|
|
147
180
|
|
|
148
181
|
/**
|
|
@@ -248,7 +281,7 @@ export interface TextRun {
|
|
|
248
281
|
|
|
249
282
|
// ── Text transform ────────────────────────────────────────────────
|
|
250
283
|
|
|
251
|
-
/** Case transform (uppercase, lowercase, capitalize).
|
|
284
|
+
/** Case transform (uppercase, lowercase, capitalize). */
|
|
252
285
|
textTransform?: TextTransform;
|
|
253
286
|
/** Force full-width characters (CJK). @todo Not yet implemented. */
|
|
254
287
|
fullWidth?: boolean;
|
|
@@ -281,6 +314,75 @@ export interface InlineWidget {
|
|
|
281
314
|
baselineOffset?: number;
|
|
282
315
|
}
|
|
283
316
|
|
|
317
|
+
// ── ListStyle (block-level list configuration) ──────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Configuration for list markers (bullet or numbered).
|
|
321
|
+
*
|
|
322
|
+
* Applies to the paragraph via `ParagraphStyle.listStyle`.
|
|
323
|
+
* Nested lists are supported via the `level` field (0-based).
|
|
324
|
+
*
|
|
325
|
+
* **Width consistency:**
|
|
326
|
+
* For `position: 'outside'`, all lines share the same indent regardless of
|
|
327
|
+
* marker width. Paragraph indentation = `bulletIndent` (or default `fontSize * 1.5`).
|
|
328
|
+
* The marker is positioned inside that zone. If the marker text is wider than
|
|
329
|
+
* the indent zone, `bulletIndent` is expanded to fit the **widest marker**
|
|
330
|
+
* across the entire list group during layout.
|
|
331
|
+
*
|
|
332
|
+
* @see {@link https://www.w3.org/TR/css-lists-3/ | CSS Lists and Counters Module Level 3}
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* // Simple bullet list
|
|
337
|
+
* { type: 'bullet', position: 'outside', bulletChar: '•' }
|
|
338
|
+
*
|
|
339
|
+
* // Numbered list starting at 5
|
|
340
|
+
* { type: 'number', numberFormat: 'decimal', startNumber: 5 }
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export interface ListStyle {
|
|
344
|
+
/** List type. */
|
|
345
|
+
type: ListType;
|
|
346
|
+
|
|
347
|
+
/** List nesting level (0-based). 0 = top-level. */
|
|
348
|
+
level?: number;
|
|
349
|
+
|
|
350
|
+
/** Marker position. Defaults to `'outside'`. */
|
|
351
|
+
position?: ListStylePosition;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Override marker character for bullet lists.
|
|
355
|
+
* If not set, defaults depend on `level`:
|
|
356
|
+
* - level 0: `'•'` (U+2022 BULLET)
|
|
357
|
+
* - level 1: `'○'` (U+25CB WHITE CIRCLE)
|
|
358
|
+
* - level 2: `'▪'` (U+25AA BLACK SMALL SQUARE)
|
|
359
|
+
*/
|
|
360
|
+
bulletChar?: string;
|
|
361
|
+
|
|
362
|
+
/** Numbering format (only used when `type === 'number'`). Defaults to `'decimal'`. */
|
|
363
|
+
numberFormat?: NumberFormat;
|
|
364
|
+
|
|
365
|
+
/** Starting number for numbered lists. Defaults to 1. */
|
|
366
|
+
startNumber?: number;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Indent in px for the marker zone.
|
|
370
|
+
* All lines of the paragraph share this indent (for `outside` position).
|
|
371
|
+
* Default: `fontSize * 1.5` (from the paragraph-level font size).
|
|
372
|
+
*
|
|
373
|
+
* For numbered lists, the engine expands this to fit the widest marker
|
|
374
|
+
* across the entire list group.
|
|
375
|
+
*/
|
|
376
|
+
bulletIndent?: number;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Per-level indent overrides (indexed by nesting level).
|
|
380
|
+
* E.g. `indents[1]` is the indent for level 1 (first nested).
|
|
381
|
+
* Falls back to `bulletIndent * (level + 1)` if not specified.
|
|
382
|
+
*/
|
|
383
|
+
indents?: number[];
|
|
384
|
+
}
|
|
385
|
+
|
|
284
386
|
// ── Paragraph (block-level) ─────────────────────────────────────────────
|
|
285
387
|
|
|
286
388
|
/**
|
|
@@ -357,6 +459,20 @@ export interface ParagraphStyle {
|
|
|
357
459
|
* - `'pre'`: preserve whitespace, wrap on newline only.
|
|
358
460
|
*/
|
|
359
461
|
whiteSpace?: WhiteSpace;
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* List marker configuration (bullet or numbered).
|
|
465
|
+
* When set, the paragraph is treated as a list item.
|
|
466
|
+
*/
|
|
467
|
+
listStyle?: ListStyle;
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Whether to restart numbering for this paragraph.
|
|
471
|
+
* Only has effect when `listStyle.type === 'number'`.
|
|
472
|
+
* When `true`, the auto-numbering counter resets to `listStyle.startNumber || 1`
|
|
473
|
+
* for this paragraph and subsequent ones in the same sequence.
|
|
474
|
+
*/
|
|
475
|
+
listRestart?: boolean;
|
|
360
476
|
}
|
|
361
477
|
|
|
362
478
|
/**
|
|
@@ -400,13 +516,23 @@ export interface Paragraph {
|
|
|
400
516
|
* use multiple `TextFrame` instances placed side-by-side.
|
|
401
517
|
*
|
|
402
518
|
* @see {@link https://www.w3.org/TR/css-multicol-1/ | CSS Multi-column Layout Level 1}
|
|
403
|
-
* @todo Not yet implemented in the layout engine.
|
|
404
519
|
*/
|
|
405
520
|
export interface MultiColumnConfig {
|
|
406
521
|
/** Number of columns (like CSS `column-count`). */
|
|
407
522
|
count: number;
|
|
408
523
|
/** Gap between columns in px (like CSS `column-gap`). */
|
|
409
524
|
gap: number;
|
|
525
|
+
/**
|
|
526
|
+
* Column fill strategy:
|
|
527
|
+
* - `'auto'`: fill columns sequentially (top-to-bottom, then next column).
|
|
528
|
+
* - `'balance'`: distribute lines evenly across columns.
|
|
529
|
+
*
|
|
530
|
+
* Defaults to `'auto'` when absent.
|
|
531
|
+
*
|
|
532
|
+
* @see {@link https://www.w3.org/TR/css-multicol-1/#cf | CSS Multi-column: column-fill}
|
|
533
|
+
* @todo `'balance'` not yet implemented.
|
|
534
|
+
*/
|
|
535
|
+
fill?: 'auto' | 'balance';
|
|
410
536
|
}
|
|
411
537
|
|
|
412
538
|
// ── TextFrame (root container) ──────────────────────────────────────────
|
package/src/types/LayoutTypes.ts
CHANGED
|
@@ -20,8 +20,10 @@ export interface Span {
|
|
|
20
20
|
text: string;
|
|
21
21
|
/** Index of the source run in the paragraph's `children` array. */
|
|
22
22
|
itemIndex: number;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/** Paragraph index in TextFrame.paragraphs[]. Stable key for grouping & diff. */
|
|
24
|
+
pIdx: number;
|
|
25
|
+
/** Optional paragraph tag (set by user via Paragraph.id). */
|
|
26
|
+
tag?: string;
|
|
25
27
|
|
|
26
28
|
/** Physical font metrics for this span */
|
|
27
29
|
fontMetrics: SpanFontMetrics;
|
|
@@ -36,10 +38,15 @@ export interface Span {
|
|
|
36
38
|
inlineWidget?: InlineWidget;
|
|
37
39
|
|
|
38
40
|
/** Per-character advance widths (for selection/tracking) */
|
|
39
|
-
glyphAdvances?: number[];
|
|
41
|
+
glyphAdvances?: number[] | Float32Array;
|
|
40
42
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Span type:
|
|
45
|
+
* - `'text'` — regular text
|
|
46
|
+
* - `'space'` — whitespace span
|
|
47
|
+
* - `'marker'` — list marker (bullet / number), rendered like text
|
|
48
|
+
*/
|
|
49
|
+
type: 'text' | 'space' | 'marker';
|
|
43
50
|
|
|
44
51
|
/**
|
|
45
52
|
* Trailing whitespace flag.
|
|
@@ -74,12 +81,19 @@ export interface SpanFontMetrics {
|
|
|
74
81
|
// ── Line (single line, formerly LineBox) ─────────────────────────────────
|
|
75
82
|
|
|
76
83
|
export interface Line {
|
|
77
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Absolute X of the line box left edge within the container.
|
|
86
|
+
* Includes outside list markers when present (marker may sit left of text).
|
|
87
|
+
*/
|
|
78
88
|
x: number;
|
|
79
89
|
/** Absolute Y of line top edge */
|
|
80
90
|
y: number;
|
|
81
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Line box width covering all spans (text + outside markers).
|
|
93
|
+
* Equals max(span.x + span.width) − min(span.x).
|
|
94
|
+
*/
|
|
82
95
|
width: number;
|
|
96
|
+
|
|
83
97
|
/** Full line height (max spans × lineHeight) */
|
|
84
98
|
height: number;
|
|
85
99
|
|
|
@@ -98,6 +112,9 @@ export interface Line {
|
|
|
98
112
|
/** Paragraph alignment (optional, for PowerPoint render) */
|
|
99
113
|
alignment?: TextAlignment;
|
|
100
114
|
|
|
115
|
+
/** Column index (0-based) when frame has multi-column layout. */
|
|
116
|
+
columnIndex?: number;
|
|
117
|
+
|
|
101
118
|
spans: Span[];
|
|
102
119
|
}
|
|
103
120
|
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime environment detection.
|
|
3
|
+
* Used to guard Node.js–only code paths (fontkit, fs, get-system-fonts)
|
|
4
|
+
* from being executed in the browser.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ Uses `globalThis.process` instead of bare `process` to avoid
|
|
7
|
+
* triggering bundler static analysis that could externalize the
|
|
8
|
+
* Node.js `process` global for browser targets.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const _process: any =
|
|
12
|
+
typeof globalThis !== 'undefined' ? (globalThis as any).process : undefined;
|
|
13
|
+
|
|
14
|
+
/** `true` when running on Node.js or Bun (has `process.versions.node`) */
|
|
15
|
+
export const isNodeLike: boolean =
|
|
16
|
+
_process != null &&
|
|
17
|
+
_process.versions != null &&
|
|
18
|
+
typeof _process.versions.node === 'string';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font utility helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provides cross‑environment helpers for loading font data in the browser
|
|
5
|
+
* (where there is no local filesystem).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Download a font file from a URL and return its bytes.
|
|
10
|
+
*
|
|
11
|
+
* Works in browser environments. In Node.js you'd typically read the file
|
|
12
|
+
* via `fs.readFileSync()` instead.
|
|
13
|
+
*
|
|
14
|
+
* @param fontUrl URL of the font file (.ttf, .otf, .woff, .woff2)
|
|
15
|
+
* @returns Font file bytes as an ArrayBuffer
|
|
16
|
+
*/
|
|
17
|
+
export async function getFontBuffer(fontUrl: string): Promise<ArrayBuffer> {
|
|
18
|
+
const response = await fetch(fontUrl);
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`[vyaz] Failed to download font from "${fontUrl}": ${response.status} ${response.statusText}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const buffer = await response.arrayBuffer();
|
|
27
|
+
return buffer;
|
|
28
|
+
}
|