@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutTypes.ts — output types (Physical Box Model).
|
|
3
|
+
*
|
|
4
|
+
* ParagraphLayoutResult → Line[] → Span[]
|
|
5
|
+
* This is the contract between layout engine and renderers.
|
|
6
|
+
*
|
|
7
|
+
* Based on plan.md §2.5 (Output — Physical Box Model / Layout Tree)
|
|
8
|
+
*/
|
|
9
|
+
import type { TextRun, InlineWidget, TextAlignment } from './Document.js';
|
|
10
|
+
export interface Span {
|
|
11
|
+
/** Offset from Line.x */
|
|
12
|
+
x: number;
|
|
13
|
+
/** Physical span width */
|
|
14
|
+
width: number;
|
|
15
|
+
/** Span text (or " " for justify spaces) */
|
|
16
|
+
text: string;
|
|
17
|
+
/** Index of the source run in the paragraph's `children` array. */
|
|
18
|
+
itemIndex: number;
|
|
19
|
+
/** Paragraph index in TextFrame.paragraphs[]. Stable key for grouping & diff. */
|
|
20
|
+
pIdx: number;
|
|
21
|
+
/** Optional paragraph tag (set by user via Paragraph.id). */
|
|
22
|
+
tag?: string;
|
|
23
|
+
/** Physical font metrics for this span */
|
|
24
|
+
fontMetrics: SpanFontMetrics;
|
|
25
|
+
/**
|
|
26
|
+
* A snapshot of the source run's style at layout time.
|
|
27
|
+
* Copied from the corresponding `TextRun` in the paragraph's `children` array.
|
|
28
|
+
*/
|
|
29
|
+
style: TextRun;
|
|
30
|
+
/** InlineWidget data (if span is an inline-box) */
|
|
31
|
+
inlineWidget?: InlineWidget;
|
|
32
|
+
/** Per-character advance widths (for selection/tracking) */
|
|
33
|
+
glyphAdvances?: number[] | Float32Array;
|
|
34
|
+
/**
|
|
35
|
+
* Span type:
|
|
36
|
+
* - `'text'` — regular text
|
|
37
|
+
* - `'space'` — whitespace span
|
|
38
|
+
* - `'marker'` — list marker (bullet / number), rendered like text
|
|
39
|
+
*/
|
|
40
|
+
type: 'text' | 'space' | 'marker';
|
|
41
|
+
/**
|
|
42
|
+
* Trailing whitespace flag.
|
|
43
|
+
* - true: span is at end of line, does not participate in line advance
|
|
44
|
+
* and is not stretched during justify (zero width for calculations).
|
|
45
|
+
* - undefined/false: regular span.
|
|
46
|
+
*
|
|
47
|
+
* See CSS Text Module Level 3 §4.1.3 (Tracking and Dropping Spaces)
|
|
48
|
+
* and Parley LineItemData::has_trailing_whitespace.
|
|
49
|
+
*/
|
|
50
|
+
trailing?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Line break mode after this span.
|
|
53
|
+
* 'soft' — soft line break (insufficient space)
|
|
54
|
+
* 'hard' — forced break (\n, explicit separator)
|
|
55
|
+
* undefined — not end of line
|
|
56
|
+
*/
|
|
57
|
+
breakType?: 'soft' | 'hard';
|
|
58
|
+
}
|
|
59
|
+
export interface SpanFontMetrics {
|
|
60
|
+
ascent: number;
|
|
61
|
+
descent: number;
|
|
62
|
+
fontSize: number;
|
|
63
|
+
/** Vertical offset from baseline (px). Used for sub/superscript positioning.
|
|
64
|
+
* Negative = above baseline (superscript). Positive = below baseline (subscript).
|
|
65
|
+
* Undefined or 0 = normal baseline position. */
|
|
66
|
+
baselineOffset?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface Line {
|
|
69
|
+
/**
|
|
70
|
+
* Absolute X of the line box left edge within the container.
|
|
71
|
+
* Includes outside list markers when present (marker may sit left of text).
|
|
72
|
+
*/
|
|
73
|
+
x: number;
|
|
74
|
+
/** Absolute Y of line top edge */
|
|
75
|
+
y: number;
|
|
76
|
+
/**
|
|
77
|
+
* Line box width covering all spans (text + outside markers).
|
|
78
|
+
* Equals max(span.x + span.width) − min(span.x).
|
|
79
|
+
*/
|
|
80
|
+
width: number;
|
|
81
|
+
/** Full line height (max spans × lineHeight) */
|
|
82
|
+
height: number;
|
|
83
|
+
/** Baseline offset from y */
|
|
84
|
+
baseline: number;
|
|
85
|
+
/** Maximum ascent in line */
|
|
86
|
+
ascent: number;
|
|
87
|
+
/** Maximum descent in line */
|
|
88
|
+
descent: number;
|
|
89
|
+
/** Index of first character in the original paragraph text */
|
|
90
|
+
startIndex: number;
|
|
91
|
+
/** Index of last character + 1 (for convenient length calculation) */
|
|
92
|
+
endIndex: number;
|
|
93
|
+
/** Paragraph alignment (optional, for PowerPoint render) */
|
|
94
|
+
alignment?: TextAlignment;
|
|
95
|
+
/** Column index (0-based) when frame has multi-column layout. */
|
|
96
|
+
columnIndex?: number;
|
|
97
|
+
spans: Span[];
|
|
98
|
+
}
|
|
99
|
+
export interface ParagraphLayoutResult {
|
|
100
|
+
width: number;
|
|
101
|
+
height: number;
|
|
102
|
+
lines: Line[];
|
|
103
|
+
/** Actual content width (text bbox, without voids) */
|
|
104
|
+
contentWidth: number;
|
|
105
|
+
/** Actual content height (text bbox) */
|
|
106
|
+
contentHeight: number;
|
|
107
|
+
}
|
|
108
|
+
/** Semantic span for snapshots (without physical metrics) */
|
|
109
|
+
export interface SemanticFragment {
|
|
110
|
+
text: string;
|
|
111
|
+
x: number;
|
|
112
|
+
width: number;
|
|
113
|
+
style?: 'bold' | 'italic' | 'normal';
|
|
114
|
+
}
|
|
115
|
+
/** Semantic line for snapshots */
|
|
116
|
+
export interface SemanticLine {
|
|
117
|
+
y: number;
|
|
118
|
+
width: number;
|
|
119
|
+
height: number;
|
|
120
|
+
baseline: number;
|
|
121
|
+
fragments: SemanticFragment[];
|
|
122
|
+
}
|
|
123
|
+
/** Semantic paragraph for YAML snapshots */
|
|
124
|
+
export interface SemanticParagraph {
|
|
125
|
+
width: number;
|
|
126
|
+
height: number;
|
|
127
|
+
lines: SemanticLine[];
|
|
128
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
/** `true` when running on Node.js or Bun (has `process.versions.node`) */
|
|
11
|
+
export declare const isNodeLike: boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
* Download a font file from a URL and return its bytes.
|
|
9
|
+
*
|
|
10
|
+
* Works in browser environments. In Node.js you'd typically read the file
|
|
11
|
+
* via `fs.readFileSync()` instead.
|
|
12
|
+
*
|
|
13
|
+
* @param fontUrl URL of the font file (.ttf, .otf, .woff, .woff2)
|
|
14
|
+
* @returns Font file bytes as an ArrayBuffer
|
|
15
|
+
*/
|
|
16
|
+
export declare function getFontBuffer(fontUrl: string): Promise<ArrayBuffer>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* groupLinesByParagraph.ts — Group lines by paragraph index.
|
|
3
|
+
*
|
|
4
|
+
* Collects all lines with the same `pIdx` into one group,
|
|
5
|
+
* regardless of their order in the array (handles multi-column
|
|
6
|
+
* where lines of the same paragraph may be split across columns).
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://www.w3.org/TR/css-text-3/ | CSS Text Module Level 3}
|
|
9
|
+
*/
|
|
10
|
+
import type { Line } from '../types/LayoutTypes.js';
|
|
11
|
+
/**
|
|
12
|
+
* A group of lines belonging to one paragraph.
|
|
13
|
+
*/
|
|
14
|
+
export interface ParagraphGroup {
|
|
15
|
+
/** Lines in this paragraph. */
|
|
16
|
+
lines: Line[];
|
|
17
|
+
/** Paragraph index in TextFrame.paragraphs[]. */
|
|
18
|
+
pIdx: number;
|
|
19
|
+
/** Optional user-assigned tag (from Paragraph.id). */
|
|
20
|
+
tag?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Group lines by `pIdx` (paragraph index).
|
|
24
|
+
*
|
|
25
|
+
* Collects **all** lines with the same `pIdx` into one group,
|
|
26
|
+
* even if they are not consecutive in the array (multi-column layout
|
|
27
|
+
* may interleave lines of different paragraphs across columns).
|
|
28
|
+
*
|
|
29
|
+
* Returns groups sorted by `pIdx` in document order.
|
|
30
|
+
* Lines without a valid `pIdx` (e.g. `pIdx === undefined`) are grouped
|
|
31
|
+
* as index `-1`.
|
|
32
|
+
*
|
|
33
|
+
* @param lines — flat array of lines from `layoutTextFrame`
|
|
34
|
+
* @returns groups of lines grouped by paragraph
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const result = layoutTextFrame(frame);
|
|
39
|
+
* const groups = groupLinesByParagraph(result.lines);
|
|
40
|
+
* // groups[0].lines → all lines for paragraph 0 (across all columns)
|
|
41
|
+
* // groups[0].pIdx → 0
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function groupLinesByParagraph(lines: Line[]): ParagraphGroup[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list.ts — helper utilities for list marker generation.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - `formatListNumber()` — format a number according to `NumberFormat`
|
|
6
|
+
* - `defaultBulletChar()` — pick a bullet character based on nesting level
|
|
7
|
+
* - `BULLET_CHARACTERS` — the Unicode characters for disc/circle/square
|
|
8
|
+
*/
|
|
9
|
+
import type { NumberFormat } from '../types/Document.js';
|
|
10
|
+
/**
|
|
11
|
+
* Unicode characters used for CSS `disc`, `circle`, `square` list-style-type
|
|
12
|
+
* values at different nesting levels.
|
|
13
|
+
*
|
|
14
|
+
* @see {@link https://www.w3.org/TR/css-lists-3/#ua-stylesheet | CSS Lists UA defaults}
|
|
15
|
+
*/
|
|
16
|
+
export declare const BULLET_CHARACTERS: Record<number, string>;
|
|
17
|
+
/**
|
|
18
|
+
* Get the default bullet character for a given nesting level.
|
|
19
|
+
*
|
|
20
|
+
* CSS UA stylesheet uses:
|
|
21
|
+
* - Level 0 (ul): disc (•)
|
|
22
|
+
* - Level 1 (ul ul): circle (○)
|
|
23
|
+
* - Level 2+ (ul ul ul): square (▪)
|
|
24
|
+
*/
|
|
25
|
+
export declare function defaultBulletChar(level: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Format a number according to the given numbering format.
|
|
28
|
+
*
|
|
29
|
+
* Supports the same formats as CSS `list-style-type`:
|
|
30
|
+
* - `decimal`: 1, 2, 3, …
|
|
31
|
+
* - `upper-roman`: I, II, III, …
|
|
32
|
+
* - `lower-roman`: i, ii, iii, …
|
|
33
|
+
* - `upper-alpha`: A, B, C, …
|
|
34
|
+
* - `lower-alpha`: a, b, c, …
|
|
35
|
+
*
|
|
36
|
+
* Follows CSS Counter Styles Level 3 algorithms.
|
|
37
|
+
* Roman numerals support the range 1–3999.
|
|
38
|
+
*
|
|
39
|
+
* @see {@link https://www.w3.org/TR/css-counter-styles-3/ | CSS Counter Styles Level 3}
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatListNumber(n: number, format: NumberFormat): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* textTransform.ts — pure text transformation functions.
|
|
3
|
+
*
|
|
4
|
+
* Implements CSS `text-transform` for: none, uppercase, lowercase, capitalize.
|
|
5
|
+
*
|
|
6
|
+
* Explicit capitalization contract (edge cases):
|
|
7
|
+
* - Apostrophe (') is NOT a word boundary: "don't stop" → "Don't Stop"
|
|
8
|
+
* - Hyphen (-) IS a word boundary: "hello-world" → "Hello-World"
|
|
9
|
+
* - Leading whitespace is preserved: " hello" → " Hello"
|
|
10
|
+
* - Numbers are not capitalized: "3d model" → "3d Model"
|
|
11
|
+
* - Unicode full-range: first `\p{L}` in each word segment gets .toUpperCase()
|
|
12
|
+
* - Empty string → ""
|
|
13
|
+
* - No letters → unchanged: "123" → "123"
|
|
14
|
+
*
|
|
15
|
+
* @see {@link https://www.w3.org/TR/css-text-3/#text-transform-property | CSS Text: text-transform}
|
|
16
|
+
*/
|
|
17
|
+
import type { TextTransform } from '../types/Document.js';
|
|
18
|
+
/**
|
|
19
|
+
* Apply text-transform to a string.
|
|
20
|
+
*
|
|
21
|
+
* @param text — input text
|
|
22
|
+
* @param transform — transform type ('none' | 'uppercase' | 'lowercase' | 'capitalize')
|
|
23
|
+
* @returns transformed text
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* transformText("hello world", 'uppercase') // → "HELLO WORLD"
|
|
28
|
+
* transformText("HELLO", 'lowercase') // → "hello"
|
|
29
|
+
* transformText("don't stop", 'capitalize') // → "Don't Stop"
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function transformText(text: string, transform?: TextTransform): string;
|
package/package.json
CHANGED
|
@@ -1,29 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vyaz/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./
|
|
6
|
-
"types": "./
|
|
7
|
-
"files": ["dist"
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"sideEffects": false,
|
|
8
9
|
"publishConfig": {
|
|
9
10
|
"access": "public"
|
|
10
11
|
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"bun": "./src/index.ts",
|
|
15
|
+
"node": "./dist/index.js",
|
|
16
|
+
"browser": "./dist/index.browser.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"browser": {
|
|
22
|
+
"get-system-fonts": false,
|
|
23
|
+
"fs": false,
|
|
24
|
+
"module": false,
|
|
25
|
+
"node:fs": false,
|
|
26
|
+
"child_process": false
|
|
27
|
+
},
|
|
11
28
|
"scripts": {
|
|
12
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
|
29
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun && bun build ./src/index.browser.ts --outdir ./dist --outfile ./dist/index.browser.js --target browser && tsc --project tsconfig.json --emitDeclarationOnly",
|
|
13
30
|
"test": "bun test"
|
|
14
31
|
},
|
|
15
32
|
"dependencies": {
|
|
16
33
|
"@chenglou/pretext": "^0.0.8",
|
|
17
|
-
"
|
|
18
|
-
"fontkit": "^2.0.4",
|
|
19
|
-
"get-system-fonts": "^2.0.2",
|
|
20
|
-
"js-yaml": "^5.0.0",
|
|
21
|
-
"typedoc": "^0.28.19"
|
|
34
|
+
"js-yaml": "^5.0.0"
|
|
22
35
|
},
|
|
23
36
|
"optionalDependencies": {
|
|
24
|
-
"@napi-rs/canvas": "^1.0.2"
|
|
37
|
+
"@napi-rs/canvas": "^1.0.2",
|
|
38
|
+
"fontkit": "^2.0.4",
|
|
39
|
+
"get-system-fonts": "^2.0.2"
|
|
25
40
|
},
|
|
26
41
|
"devDependencies": {
|
|
27
|
-
"@types/node": "^26.0.0"
|
|
42
|
+
"@types/node": "^26.0.0",
|
|
43
|
+
"@clean-jsdoc-theme/typedoc": "^5.0.6",
|
|
44
|
+
"typedoc": "^0.28.19"
|
|
28
45
|
}
|
|
29
|
-
}
|
|
46
|
+
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DocumentCompiler.ts — compile Paragraph → PreparedRichInlineItem[].
|
|
3
|
-
*
|
|
4
|
-
* Each TextRun becomes a RichInlineItem for pretext.
|
|
5
|
-
* inline-box: text → \uFFFC, dimensions in metadata.inlineWidget.
|
|
6
|
-
* super/sub: fontSize *= 0.65, baselineOffset in metadata.
|
|
7
|
-
*
|
|
8
|
-
* Simple JSON-serialisable format — does not depend on pretext directly.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Paragraph, TextRun } from '../types/Document.js';
|
|
12
|
-
import { DEFAULT_TEXT_STYLE } from '../types/Document.js';
|
|
13
|
-
|
|
14
|
-
// ── Font Weight normalization (matching react-pdf convention) ───────────
|
|
15
|
-
|
|
16
|
-
export const FONT_WEIGHTS: Record<string, number> = {
|
|
17
|
-
thin: 100,
|
|
18
|
-
hairline: 100,
|
|
19
|
-
ultralight: 200,
|
|
20
|
-
extralight: 200,
|
|
21
|
-
light: 300,
|
|
22
|
-
normal: 400,
|
|
23
|
-
medium: 500,
|
|
24
|
-
semibold: 600,
|
|
25
|
-
demibold: 600,
|
|
26
|
-
bold: 700,
|
|
27
|
-
ultrabold: 800,
|
|
28
|
-
extrabold: 800,
|
|
29
|
-
heavy: 900,
|
|
30
|
-
black: 900,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/** Normalize fontWeight to a numeric value (400 by default). */
|
|
34
|
-
export function normalizeFontWeight(weight: number | string | undefined): number {
|
|
35
|
-
if (weight == null) return FONT_WEIGHTS.normal;
|
|
36
|
-
if (typeof weight === 'number') return weight;
|
|
37
|
-
return FONT_WEIGHTS[weight.toLowerCase()] ?? FONT_WEIGHTS.normal;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Font token for pretext: "${style}_${weight}_${fontSize}_${family}" */
|
|
41
|
-
export function makeFontToken(run: TextRun, effectiveFontSize: number): string {
|
|
42
|
-
const fontStyle = run.fontStyle || DEFAULT_TEXT_STYLE.fontStyle || 'normal';
|
|
43
|
-
const fontWeight = normalizeFontWeight(run.fontWeight ?? DEFAULT_TEXT_STYLE.fontWeight);
|
|
44
|
-
const fontFamily = run.fontFamily || DEFAULT_TEXT_STYLE.fontFamily || 'Arial';
|
|
45
|
-
return `${fontStyle} ${fontWeight} ${effectiveFontSize}px ${fontFamily}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Compilation context (passed to pretext) */
|
|
49
|
-
export interface PreparedRichInlineItem {
|
|
50
|
-
text: string;
|
|
51
|
-
font: string;
|
|
52
|
-
letterSpacing?: number;
|
|
53
|
-
extraWidth?: number; // padding, border for inline-box
|
|
54
|
-
break?: 'normal' | 'never'; // for atomic chips
|
|
55
|
-
metadata: {
|
|
56
|
-
originalRunIndex: number;
|
|
57
|
-
baselineOffset: number;
|
|
58
|
-
effectiveFontSize: number;
|
|
59
|
-
style: TextRun;
|
|
60
|
-
inlineWidget?: TextRun['inlineWidget'];
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const SUPER_SUB_SCALE = 0.65;
|
|
65
|
-
const SUPER_OFFSET_RATIO = -0.4;
|
|
66
|
-
const SUB_OFFSET_RATIO = 0.15;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Compile a paragraph into PreparedRichInlineItem[].
|
|
70
|
-
*/
|
|
71
|
-
export function compileParagraph(paragraph: Paragraph): PreparedRichInlineItem[] {
|
|
72
|
-
const items: PreparedRichInlineItem[] = [];
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < paragraph.children.length; i++) {
|
|
75
|
-
const run = paragraph.children[i];
|
|
76
|
-
|
|
77
|
-
// Compute effective fontSize and baselineOffset
|
|
78
|
-
const baseFontSize = run.fontSize ?? DEFAULT_TEXT_STYLE.fontSize ?? 12;
|
|
79
|
-
let effectiveFontSize = baseFontSize;
|
|
80
|
-
let baselineOffset = 0;
|
|
81
|
-
|
|
82
|
-
if (run.script === 'super') {
|
|
83
|
-
effectiveFontSize = baseFontSize * SUPER_SUB_SCALE;
|
|
84
|
-
baselineOffset = baseFontSize * SUPER_OFFSET_RATIO;
|
|
85
|
-
} else if (run.script === 'sub') {
|
|
86
|
-
effectiveFontSize = baseFontSize * SUPER_SUB_SCALE;
|
|
87
|
-
baselineOffset = baseFontSize * SUB_OFFSET_RATIO;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Inline-box: text → \uFFFC
|
|
91
|
-
const text = run.type === 'inline-box' ? '\uFFFC' : run.text;
|
|
92
|
-
|
|
93
|
-
// Normalize fontWeight to numeric value
|
|
94
|
-
const resolvedFontWeight = normalizeFontWeight(run.fontWeight ?? DEFAULT_TEXT_STYLE.fontWeight);
|
|
95
|
-
|
|
96
|
-
// Fill missing style fields from DEFAULT_TEXT_STYLE
|
|
97
|
-
const resolvedStyle: TextRun = {
|
|
98
|
-
...DEFAULT_TEXT_STYLE,
|
|
99
|
-
...run,
|
|
100
|
-
fontSize: effectiveFontSize,
|
|
101
|
-
fontWeight: resolvedFontWeight,
|
|
102
|
-
text: run.text,
|
|
103
|
-
type: run.type,
|
|
104
|
-
} as TextRun;
|
|
105
|
-
|
|
106
|
-
const item: PreparedRichInlineItem = {
|
|
107
|
-
text,
|
|
108
|
-
font: makeFontToken(run, effectiveFontSize),
|
|
109
|
-
letterSpacing: run.letterSpacing,
|
|
110
|
-
metadata: {
|
|
111
|
-
originalRunIndex: i,
|
|
112
|
-
baselineOffset,
|
|
113
|
-
effectiveFontSize,
|
|
114
|
-
style: resolvedStyle,
|
|
115
|
-
inlineWidget: run.inlineWidget,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Inline-box: add extraWidth and break: 'never'
|
|
120
|
-
if (run.type === 'inline-box' && run.inlineWidget) {
|
|
121
|
-
item.extraWidth = run.inlineWidget.width;
|
|
122
|
-
item.break = 'never';
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
items.push(item);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return items;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Get the full text of a paragraph (for INDEX_CONSIST checks).
|
|
133
|
-
*/
|
|
134
|
-
export function getParagraphText(paragraph: Paragraph): string {
|
|
135
|
-
return paragraph.children.map(r => r.text).join('');
|
|
136
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vyaz/core — Public API.
|
|
3
|
-
*
|
|
4
|
-
* Exports input types (Logical level), output types (Physical Box Model),
|
|
5
|
-
* layout engines, font metric providers, renderers, and utilities.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ── Input types (Logical level) ─────────────────────────────────────────
|
|
9
|
-
export type {
|
|
10
|
-
TextFrame,
|
|
11
|
-
Paragraph,
|
|
12
|
-
ParagraphStyle,
|
|
13
|
-
TextRun,
|
|
14
|
-
InlineWidget,
|
|
15
|
-
AutofitConfig,
|
|
16
|
-
TextAlignment,
|
|
17
|
-
WritingMode,
|
|
18
|
-
TextOrientation,
|
|
19
|
-
VerticalAlignment,
|
|
20
|
-
ScriptType,
|
|
21
|
-
WhiteSpace,
|
|
22
|
-
MultiColumnConfig,
|
|
23
|
-
DominantBaseline,
|
|
24
|
-
LineFitEdge,
|
|
25
|
-
TextAlignLast,
|
|
26
|
-
WordBreak,
|
|
27
|
-
LineBreak,
|
|
28
|
-
OverflowWrap,
|
|
29
|
-
TextDecorationStyle,
|
|
30
|
-
TextTransform,
|
|
31
|
-
} from './types/Document.js';
|
|
32
|
-
export {
|
|
33
|
-
DEFAULT_PARAGRAPH_STYLE,
|
|
34
|
-
DEFAULT_TEXT_STYLE,
|
|
35
|
-
} from './types/Document.js';
|
|
36
|
-
|
|
37
|
-
// ── Output types (Physical Box Model) ───────────────────────────────────
|
|
38
|
-
export type {
|
|
39
|
-
ParagraphLayoutResult,
|
|
40
|
-
Line,
|
|
41
|
-
Span,
|
|
42
|
-
SpanFontMetrics,
|
|
43
|
-
SemanticParagraph,
|
|
44
|
-
SemanticLine,
|
|
45
|
-
SemanticFragment,
|
|
46
|
-
} from './types/LayoutTypes.js';
|
|
47
|
-
|
|
48
|
-
// ── Font types ──────────────────────────────────────────────────────────
|
|
49
|
-
export type {
|
|
50
|
-
FontMetrics,
|
|
51
|
-
IFontMetricsProvider,
|
|
52
|
-
GlyphData,
|
|
53
|
-
} from './types/FontTypes.js';
|
|
54
|
-
|
|
55
|
-
// ── Layout Engine ───────────────────────────────────────────────────────
|
|
56
|
-
export { ParagraphLayoutEngine, paragraphLayoutEngine } from './layout/ParagraphLayoutEngine.js';
|
|
57
|
-
export { positionLines } from './layout/PositioningEngine.js';
|
|
58
|
-
export { assertLineInvariants, linesToYAML } from './layout/LineBoxValidator.js';
|
|
59
|
-
export type { InvariantError } from './layout/LineBoxValidator.js';
|
|
60
|
-
|
|
61
|
-
// ── TextFrame Layout Engine ──────────────────────────────────────────────
|
|
62
|
-
export { layoutTextFrame } from './layout/TextFrameLayoutEngine.js';
|
|
63
|
-
export type { TextFrameLayoutResult } from './layout/TextFrameLayoutEngine.js';
|
|
64
|
-
|
|
65
|
-
// ── Autofit ─────────────────────────────────────────────────────────────
|
|
66
|
-
export { applyScale, findScale } from './layout/AutoFitEngine.js';
|
|
67
|
-
export type { AutoFitOptions, AutoFitResult } from './layout/AutoFitEngine.js';
|
|
68
|
-
|
|
69
|
-
// ── Compiler ────────────────────────────────────────────────────────────
|
|
70
|
-
export { compileParagraph, getParagraphText, makeFontToken } from './compile/DocumentCompiler.js';
|
|
71
|
-
export type { PreparedRichInlineItem } from './compile/DocumentCompiler.js';
|
|
72
|
-
|
|
73
|
-
// ── Font metrics ────────────────────────────────────────────────────────
|
|
74
|
-
export { FontMetricsProvider, fontMetricsProvider } from './measure/FontMetricsProvider.js';
|
|
75
|
-
|
|
76
|
-
// ── System font registry ────────────────────────────────────────────────
|
|
77
|
-
export { SystemFontRegistry, systemFontRegistry } from './measure/SystemFontRegistry.js';
|
|
78
|
-
|
|
79
|
-
// ── Errors ──────────────────────────────────────────────────────────────
|
|
80
|
-
export { FontNotFoundError } from './measure/FontNotFoundError.js';
|
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
|
|
14
|
-
import type { TextFrame } from '../types/Document.js';
|
|
15
|
-
|
|
16
|
-
/** Autofit options */
|
|
17
|
-
export interface AutoFitOptions {
|
|
18
|
-
minScale?: number; // minimum scale (default 0.1)
|
|
19
|
-
tolerance?: number; // binary search tolerance (default 0.01)
|
|
20
|
-
maxIterations?: number; // max iterations (default 50)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Autofit result */
|
|
24
|
-
export interface AutoFitResult {
|
|
25
|
-
scaleFactor: number; // 0.0 … 1.0
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Apply a scale factor to all fontSize values in the document.
|
|
30
|
-
* inlineWidget dimensions are NOT scaled.
|
|
31
|
-
* Returns a NEW document (does not mutate the original).
|
|
32
|
-
*/
|
|
33
|
-
export function applyScale(
|
|
34
|
-
doc: TextFrame,
|
|
35
|
-
scale: number,
|
|
36
|
-
): TextFrame {
|
|
37
|
-
const clone = JSON.parse(JSON.stringify(doc)) as TextFrame;
|
|
38
|
-
|
|
39
|
-
for (const paragraph of clone.paragraphs) {
|
|
40
|
-
for (const run of paragraph.children) {
|
|
41
|
-
run.fontSize = Math.round(run.fontSize * scale * 100) / 100;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (clone.defaultStyle?.fontSize) {
|
|
46
|
-
clone.defaultStyle.fontSize = Math.round(clone.defaultStyle.fontSize * scale * 100) / 100;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return clone;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Find the optimal scale factor for a document.
|
|
54
|
-
*
|
|
55
|
-
* @param doc — source document
|
|
56
|
-
* @param layoutFn — layout(doc) → { height: number; width: number }
|
|
57
|
-
* @param config — autofit maxWidth/maxHeight
|
|
58
|
-
* @param options — search precision
|
|
59
|
-
*/
|
|
60
|
-
export function findScale(
|
|
61
|
-
doc: TextFrame,
|
|
62
|
-
layoutFn: (scaledDoc: TextFrame) => { height: number; width: number },
|
|
63
|
-
config: { maxWidth: number; maxHeight: number },
|
|
64
|
-
options?: AutoFitOptions,
|
|
65
|
-
): AutoFitResult {
|
|
66
|
-
const minScale = options?.minScale ?? 0.1;
|
|
67
|
-
const tolerance = options?.tolerance ?? 0.01;
|
|
68
|
-
const maxIterations = options?.maxIterations ?? 50;
|
|
69
|
-
const maxHeight = config.maxHeight;
|
|
70
|
-
const maxWidth = config.maxWidth;
|
|
71
|
-
|
|
72
|
-
// Check original size
|
|
73
|
-
const origResult = layoutFn(doc);
|
|
74
|
-
if (origResult.height <= maxHeight && origResult.width <= maxWidth) {
|
|
75
|
-
return { scaleFactor: 1 };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Binary search
|
|
79
|
-
let lo = minScale;
|
|
80
|
-
let hi = 1.0;
|
|
81
|
-
let best = minScale;
|
|
82
|
-
|
|
83
|
-
for (let iter = 0; iter < maxIterations; iter++) {
|
|
84
|
-
const mid = (lo + hi) / 2;
|
|
85
|
-
const scaledDoc = applyScale(doc, mid);
|
|
86
|
-
const result = layoutFn(scaledDoc);
|
|
87
|
-
|
|
88
|
-
if (result.height <= maxHeight && result.width <= maxWidth) {
|
|
89
|
-
// scale is valid — try larger
|
|
90
|
-
best = mid;
|
|
91
|
-
lo = mid + tolerance / 2;
|
|
92
|
-
} else {
|
|
93
|
-
// scale is invalid — try smaller
|
|
94
|
-
hi = mid - tolerance / 2;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (hi - lo < tolerance) break;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return { scaleFactor: Math.round(best * 100) / 100 };
|
|
101
|
-
}
|