pretrunc 0.0.0-stage → 0.1.0
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/CHANGELOG.md +7 -0
- package/DESIGN.md +235 -0
- package/LICENSE +21 -0
- package/README.md +200 -2
- package/dist/cut-D7qi2yZl.js +356 -0
- package/dist/cut-D7qi2yZl.js.map +1 -0
- package/dist/cut.d.ts +75 -0
- package/dist/dom-CPL0-qc3.js +52 -0
- package/dist/dom-CPL0-qc3.js.map +1 -0
- package/dist/dom.d.ts +22 -0
- package/dist/dom.js +1 -0
- package/dist/face-pICHAjvk.js +68 -0
- package/dist/face-pICHAjvk.js.map +1 -0
- package/dist/face.d.ts +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/lines-DOj2DQLl.js +334 -0
- package/dist/lines-DOj2DQLl.js.map +1 -0
- package/dist/lines.d.ts +32 -0
- package/dist/lines.js +1 -0
- package/dist/metrics-C2jv4JVL.js +49 -0
- package/dist/metrics-C2jv4JVL.js.map +1 -0
- package/dist/metrics-cli.js +19 -0
- package/dist/metrics-cli.js.map +1 -0
- package/dist/metrics.d.ts +36 -0
- package/dist/metrics.js +73 -0
- package/dist/metrics.js.map +1 -0
- package/dist/react-CnQfjWyS.js +156 -0
- package/dist/react-CnQfjWyS.js.map +1 -0
- package/dist/react-lines.d.ts +27 -0
- package/dist/react-lines.js +74 -0
- package/dist/react-lines.js.map +1 -0
- package/dist/react.d.ts +81 -0
- package/dist/react.js +1 -0
- package/dist/solid-BWC32VMg.js +155 -0
- package/dist/solid-BWC32VMg.js.map +1 -0
- package/dist/solid-lines.d.ts +27 -0
- package/dist/solid-lines.js +90 -0
- package/dist/solid-lines.js.map +1 -0
- package/dist/solid.d.ts +77 -0
- package/dist/solid.js +1 -0
- package/package.json +145 -3
- package/src/cut.ts +596 -0
- package/src/dom.ts +59 -0
- package/src/face.ts +112 -0
- package/src/index.ts +15 -0
- package/src/lines.ts +585 -0
- package/src/metrics.ts +106 -0
- package/src/react-lines.ts +99 -0
- package/src/react.ts +251 -0
- package/src/solid-lines.tsx +89 -0
- package/src/solid.tsx +225 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"face-pICHAjvk.js","names":[],"sources":["../src/face.ts"],"sourcesContent":["declare const opaque: unique symbol;\n\n/**\n * Measures text in one font: `canvasFace`, `tableFace` from a metrics table, or\n * `face` over your own measure function. Its caches are pretrunc's, so the type shows\n * none of them.\n */\nexport interface Face {\n readonly [opaque]: true;\n}\n\n/** A face's measure function and caches, for pretrunc's own modules. */\nexport interface FaceData extends Face {\n /** The width of a string in px, as one run of text. */\n readonly m: (text: string) => number;\n /** Widths already measured, by text: graphemes, grapheme pairs, words and ellipses. */\n readonly c: Map<string, number>;\n /** Widths of single characters below U+0300 by code, NaN until measured. */\n readonly u: Float64Array;\n /** Widths of pairs of characters below U+0300, by `first * 768 + second`. */\n readonly p: Map<number, number>;\n /** Widths of pairs of ASCII characters, by `first * 128 + second`, NaN until measured. */\n readonly q: Float64Array;\n /**\n * Kerning beside each ellipsis, by ellipsis: `[code]` between a character below U+0300\n * and the ellipsis after it, `[768 + code]` between the ellipsis and the character.\n * Shared by every text in the face. NaN until measured.\n */\n readonly e: Map<string, Float64Array>;\n}\n\nconst faceOver = (measure: (text: string) => number) =>\n ({\n m: measure,\n c: new Map(),\n u: new Float64Array(768).fill(Number.NaN),\n p: new Map(),\n q: new Float64Array(16384).fill(Number.NaN),\n e: new Map(),\n }) as FaceData;\n\n/** A face over your own measure function: node-canvas, a test double. */\nexport const face: (measure: (text: string) => number) => Face = faceOver;\n\nlet context: OffscreenCanvasRenderingContext2D | undefined;\n// Setting `font` parses it every time, and reading it back gives a normalised string\n// that differs from what was set, so the last font set is kept here instead.\nlet current = \"\";\nconst faces = new Map<string, FaceData>();\n\n/** The canvas face for a CSS font shorthand, for pretrunc's own modules. */\nexport const faceOf = (font: string): FaceData => {\n let f = faces.get(font);\n if (!f) {\n f = faceOver((text) => {\n context ??= new OffscreenCanvas(0, 0).getContext(\"2d\")!;\n if (current !== font) context.font = current = font;\n return context.measureText(text).width;\n });\n faces.set(font, f);\n }\n return f;\n};\n\n/** The canvas face for a CSS font shorthand, shared by everything in that font. */\nexport const canvasFace: (font: string) => Face = faceOf;\n\n/**\n * Drops the canvas face for `font`, or every one, and with `pretrunc/lines` loaded\n * every width pretext keeps, so text prepared afterwards is measured again. Call it\n * once a web font loads: widths measured before are the fallback font's.\n */\nexport const forget = (font?: string): void => {\n if (font === undefined) faces.clear();\n else faces.delete(font);\n for (const clear of clears) clear();\n};\n\n// Caches beyond the core's that hold widths too: pretext's, which `pretrunc/lines`\n// registers so the core never imports pretext. pretext can't drop one font's widths,\n// so a `forget` of one font drops all of its.\nconst clears: Array<() => void> = [];\nexport const alsoForget = (clear: () => void): void => void clears.push(clear);\n\n/** The width of `text` in `f`, measured once. */\nexport const widthOf = (f: FaceData, text: string): number => {\n let w = f.c.get(text);\n if (w === undefined) f.c.set(text, (w = f.m(text)));\n return w;\n};\n\n// Most text is in characters below U+0300, one UTF-16 unit each, so their widths are\n// found by code: no string is built unless one has to be measured.\nexport const unitOf = (f: FaceData, code: number): number => {\n let w = f.u[code]!;\n // NaN, not yet measured.\n if (w !== w) f.u[code] = w = f.m(String.fromCharCode(code));\n return w;\n};\nexport const pairOf = (f: FaceData, first: number, second: number): number => {\n // ASCII pairs, most of most text, come from a flat table, the rest from a map.\n if (first < 128 && second < 128) {\n const key = first * 128 + second;\n let w = f.q[key]!;\n if (w !== w) f.q[key] = w = f.m(String.fromCharCode(first, second));\n return w;\n }\n const key = first * 768 + second;\n let w = f.p.get(key);\n if (w === undefined) f.p.set(key, (w = f.m(String.fromCharCode(first, second))));\n return w;\n};\n"],"mappings":";AA+BA,IAAM,YAAY,aACf;CACC,GAAG;CACH,mBAAG,IAAI,IAAI;CACX,oBAAG,IAAI,aAAa,GAAG,EAAA,CAAE,KAAK,GAAU;CACxC,mBAAG,IAAI,IAAI;CACX,oBAAG,IAAI,aAAa,KAAK,EAAA,CAAE,KAAK,GAAU;CAC1C,mBAAG,IAAI,IAAI;AACb;;AAGF,IAAa,OAAoD;AAEjE,IAAI;AAGJ,IAAI,UAAU;AACd,IAAM,wBAAQ,IAAI,IAAsB;;AAGxC,IAAa,UAAU,SAA2B;CAChD,IAAI,IAAI,MAAM,IAAI,IAAI;CACtB,IAAI,CAAC,GAAG;EACN,IAAI,UAAU,SAAS;GACrB,YAAY,IAAI,gBAAgB,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI;GACrD,IAAI,YAAY,MAAM,QAAQ,OAAO,UAAU;GAC/C,OAAO,QAAQ,YAAY,IAAI,CAAC,CAAC;EACnC,CAAC;EACD,MAAM,IAAI,MAAM,CAAC;CACnB;CACA,OAAO;AACT;;AAGA,IAAa,aAAqC;;;;;;AAOlD,IAAa,UAAU,SAAwB;CAC7C,IAAI,SAAS,KAAA,GAAW,MAAM,MAAM;MAC/B,MAAM,OAAO,IAAI;CACtB,KAAK,MAAM,SAAS,QAAQ,MAAM;AACpC;AAKA,IAAM,SAA4B,CAAC;AACnC,IAAa,cAAc,UAA4B,KAAK,OAAO,KAAK,KAAK;;AAG7E,IAAa,WAAW,GAAa,SAAyB;CAC5D,IAAI,IAAI,EAAE,EAAE,IAAI,IAAI;CACpB,IAAI,MAAM,KAAA,GAAW,EAAE,EAAE,IAAI,MAAO,IAAI,EAAE,EAAE,IAAI,CAAE;CAClD,OAAO;AACT;AAIA,IAAa,UAAU,GAAa,SAAyB;CAC3D,IAAI,IAAI,EAAE,EAAE;CAEZ,IAAI,MAAM,GAAG,EAAE,EAAE,QAAQ,IAAI,EAAE,EAAE,OAAO,aAAa,IAAI,CAAC;CAC1D,OAAO;AACT;AACA,IAAa,UAAU,GAAa,OAAe,WAA2B;CAE5E,IAAI,QAAQ,OAAO,SAAS,KAAK;EAC/B,MAAM,MAAM,QAAQ,MAAM;EAC1B,IAAI,IAAI,EAAE,EAAE;EACZ,IAAI,MAAM,GAAG,EAAE,EAAE,OAAO,IAAI,EAAE,EAAE,OAAO,aAAa,OAAO,MAAM,CAAC;EAClE,OAAO;CACT;CACA,MAAM,MAAM,QAAQ,MAAM;CAC1B,IAAI,IAAI,EAAE,EAAE,IAAI,GAAG;CACnB,IAAI,MAAM,KAAA,GAAW,EAAE,EAAE,IAAI,KAAM,IAAI,EAAE,EAAE,OAAO,aAAa,OAAO,MAAM,CAAC,CAAE;CAC/E,OAAO;AACT"}
|
package/dist/face.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
declare const opaque: unique symbol;
|
|
2
|
+
/**
|
|
3
|
+
* Measures text in one font: `canvasFace`, `tableFace` from a metrics table, or
|
|
4
|
+
* `face` over your own measure function. Its caches are pretrunc's, so the type shows
|
|
5
|
+
* none of them.
|
|
6
|
+
*/
|
|
7
|
+
export interface Face {
|
|
8
|
+
readonly [opaque]: true;
|
|
9
|
+
}
|
|
10
|
+
/** A face's measure function and caches, for pretrunc's own modules. */
|
|
11
|
+
export interface FaceData extends Face {
|
|
12
|
+
/** The width of a string in px, as one run of text. */
|
|
13
|
+
readonly m: (text: string) => number;
|
|
14
|
+
/** Widths already measured, by text: graphemes, grapheme pairs, words and ellipses. */
|
|
15
|
+
readonly c: Map<string, number>;
|
|
16
|
+
/** Widths of single characters below U+0300 by code, NaN until measured. */
|
|
17
|
+
readonly u: Float64Array;
|
|
18
|
+
/** Widths of pairs of characters below U+0300, by `first * 768 + second`. */
|
|
19
|
+
readonly p: Map<number, number>;
|
|
20
|
+
/** Widths of pairs of ASCII characters, by `first * 128 + second`, NaN until measured. */
|
|
21
|
+
readonly q: Float64Array;
|
|
22
|
+
/**
|
|
23
|
+
* Kerning beside each ellipsis, by ellipsis: `[code]` between a character below U+0300
|
|
24
|
+
* and the ellipsis after it, `[768 + code]` between the ellipsis and the character.
|
|
25
|
+
* Shared by every text in the face. NaN until measured.
|
|
26
|
+
*/
|
|
27
|
+
readonly e: Map<string, Float64Array>;
|
|
28
|
+
}
|
|
29
|
+
/** A face over your own measure function: node-canvas, a test double. */
|
|
30
|
+
export declare const face: (measure: (text: string) => number) => Face;
|
|
31
|
+
/** The canvas face for a CSS font shorthand, for pretrunc's own modules. */
|
|
32
|
+
export declare const faceOf: (font: string) => FaceData;
|
|
33
|
+
/** The canvas face for a CSS font shorthand, shared by everything in that font. */
|
|
34
|
+
export declare const canvasFace: (font: string) => Face;
|
|
35
|
+
/**
|
|
36
|
+
* Drops the canvas face for `font`, or every one, and with `pretrunc/lines` loaded
|
|
37
|
+
* every width pretext keeps, so text prepared afterwards is measured again. Call it
|
|
38
|
+
* once a web font loads: widths measured before are the fallback font's.
|
|
39
|
+
*/
|
|
40
|
+
export declare const forget: (font?: string) => void;
|
|
41
|
+
export declare const alsoForget: (clear: () => void) => void;
|
|
42
|
+
/** The width of `text` in `f`, measured once. */
|
|
43
|
+
export declare const widthOf: (f: FaceData, text: string) => number;
|
|
44
|
+
export declare const unitOf: (f: FaceData, code: number) => number;
|
|
45
|
+
export declare const pairOf: (f: FaceData, first: number, second: number) => number;
|
|
46
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { c as widthOf, i as faceOf, t as alsoForget } from "./face-pICHAjvk.js";
|
|
2
|
+
import { i as pack, n as cutFrom, o as prepareRange, r as head } from "./cut-D7qi2yZl.js";
|
|
3
|
+
import { clearCache, layoutNextLineRange, materializeLineRange, prepareWithSegments, walkLineRanges } from "@chenglou/pretext";
|
|
4
|
+
import { layoutNextRichInlineLineRange, materializeRichInlineLineRange, prepareRichInline, walkRichInlineLineRanges } from "@chenglou/pretext/rich-inline";
|
|
5
|
+
//#region src/lines.ts
|
|
6
|
+
alsoForget(clearCache);
|
|
7
|
+
var START = {
|
|
8
|
+
segmentIndex: 0,
|
|
9
|
+
graphemeIndex: 0
|
|
10
|
+
};
|
|
11
|
+
var segmenter;
|
|
12
|
+
var list = /* @__PURE__ */ new Int32Array(64);
|
|
13
|
+
var listed = 0;
|
|
14
|
+
var graphemesOf = (text) => [...(segmenter ??= new Intl.Segmenter()).segment(text)];
|
|
15
|
+
var engineWidth = (face, spacing, text, after = "") => {
|
|
16
|
+
text = text.replace(/[ \n]+$/, "");
|
|
17
|
+
const next = graphemesOf(after.slice(0, 16))[0]?.segment;
|
|
18
|
+
return text.includes(" ") ? 0 : Math.max(widthOf(face, text), next ? widthOf(face, text + next) - widthOf(face, next) : 0) + spacing * graphemesOf(text).length;
|
|
19
|
+
};
|
|
20
|
+
var settleLine = (line, room, inside, edge, next, widthOf) => {
|
|
21
|
+
for (let fit = room, tries = 0; line && (inside || line.end.graphemeIndex || line.width > edge) && tries < 8; tries++) {
|
|
22
|
+
const over = Math.max(line.width, widthOf(line)) - room;
|
|
23
|
+
if (over <= 0) break;
|
|
24
|
+
line = next(fit = Math.min(fit - over, line.width) - .001) ?? line;
|
|
25
|
+
}
|
|
26
|
+
return line;
|
|
27
|
+
};
|
|
28
|
+
var nextLine = (p, face, spacing, room, c, edge = room) => {
|
|
29
|
+
const line = layoutNextLineRange(p, room, c);
|
|
30
|
+
return line && (c.graphemeIndex || line.end.graphemeIndex || line.width > edge) ? settleLine(line, room, c.graphemeIndex > 0, edge, (room) => layoutNextLineRange(p, room, c), (line) => engineWidth(face, spacing, materializeLineRange(p, line).text, graphemesOf(p.segments[line.end.segmentIndex] ?? "")[line.end.graphemeIndex]?.segment)) : line;
|
|
31
|
+
};
|
|
32
|
+
var startsOf = (p, graphemes) => {
|
|
33
|
+
const known = graphemes ? p.d : p.c;
|
|
34
|
+
if (known) return known;
|
|
35
|
+
const { l, s, text } = p;
|
|
36
|
+
const size = (graphemes ? text.length : l.segments.length) + 1;
|
|
37
|
+
const at = new Int32Array(size);
|
|
38
|
+
const seg = new Int32Array(size);
|
|
39
|
+
const g = new Int32Array(size);
|
|
40
|
+
const f = new Uint8Array(size);
|
|
41
|
+
let n = 0;
|
|
42
|
+
for (let i = 0; i < l.segments.length; i++) {
|
|
43
|
+
const segment = l.segments[i];
|
|
44
|
+
const space = /^\s+$/.test(segment);
|
|
45
|
+
let k = 0;
|
|
46
|
+
for (const grapheme of graphemes ? graphemesOf(segment) : [{ index: 0 }]) {
|
|
47
|
+
const offset = s[i] + grapheme.index;
|
|
48
|
+
const code = text.charCodeAt(offset);
|
|
49
|
+
at[n] = offset;
|
|
50
|
+
seg[n] = i;
|
|
51
|
+
g[n] = k++;
|
|
52
|
+
f[n++] = +(space || code === 44 || code === 58 || code === 59);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const starts = {
|
|
56
|
+
at,
|
|
57
|
+
seg,
|
|
58
|
+
g,
|
|
59
|
+
f,
|
|
60
|
+
n
|
|
61
|
+
};
|
|
62
|
+
return graphemes ? p.d = starts : p.c = starts;
|
|
63
|
+
};
|
|
64
|
+
var prepareLines = (text, font, options = {}) => {
|
|
65
|
+
const q = {
|
|
66
|
+
...options.whiteSpace ? { whiteSpace: options.whiteSpace } : {},
|
|
67
|
+
...options.wordBreak ? { wordBreak: options.wordBreak } : {},
|
|
68
|
+
...options.letterSpacing ? { letterSpacing: options.letterSpacing } : {}
|
|
69
|
+
};
|
|
70
|
+
const l = prepareWithSegments(text, font, q);
|
|
71
|
+
let joined = "";
|
|
72
|
+
const s = [0];
|
|
73
|
+
for (const segment of l.segments) s.push((joined += segment).length);
|
|
74
|
+
const face = faceOf(font);
|
|
75
|
+
const tab = options.whiteSpace === "pre-wrap" ? 8 * (widthOf(face, " ") + (options.letterSpacing ?? 0)) + widthOf(face, "0") / 2 : 0;
|
|
76
|
+
return {
|
|
77
|
+
text: joined,
|
|
78
|
+
l,
|
|
79
|
+
font,
|
|
80
|
+
s,
|
|
81
|
+
o: options,
|
|
82
|
+
q,
|
|
83
|
+
tab,
|
|
84
|
+
r: void 0,
|
|
85
|
+
c: void 0,
|
|
86
|
+
d: void 0
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/** The room in px after the last line of the whole text, where a "less" link would go. */
|
|
90
|
+
var roomAfter = (lines, width) => {
|
|
91
|
+
let last = 0;
|
|
92
|
+
walkLineRanges(lines.l, width, (line) => last = line.width);
|
|
93
|
+
return width - last;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Cuts text to `options.lines` lines of `width` px, with the ellipsis at the end of the
|
|
97
|
+
* last line, the start of the first, or inside a middle line (`ratio` of the way through).
|
|
98
|
+
*/
|
|
99
|
+
var clamp = (lines, width, options) => {
|
|
100
|
+
const p = lines;
|
|
101
|
+
const { text, l, font, s, o } = p;
|
|
102
|
+
const { at = "end", ratio = .5, words = true, trim = true, ellipsis = "…", after = 0 } = options;
|
|
103
|
+
const n = Math.max(1, options.lines);
|
|
104
|
+
const length = text.length;
|
|
105
|
+
const spacing = o.letterSpacing ?? 0;
|
|
106
|
+
const face = faceOf(font);
|
|
107
|
+
const ew = typeof ellipsis === "number" ? ellipsis : widthOf(face, ellipsis) + spacing * ellipsis.length;
|
|
108
|
+
const next = (room, c) => nextLine(l, face, spacing, room, c);
|
|
109
|
+
const offset = (c) => s[c.segmentIndex] + (c.graphemeIndex ? graphemesOf(l.segments[c.segmentIndex])[c.graphemeIndex].index : 0);
|
|
110
|
+
const linesFrom = (from, first, limit, extra = after) => {
|
|
111
|
+
let lines = 0;
|
|
112
|
+
let c = from;
|
|
113
|
+
let room = first;
|
|
114
|
+
let last = 0;
|
|
115
|
+
while (lines <= limit) {
|
|
116
|
+
const range = next(room, c);
|
|
117
|
+
if (range === null) break;
|
|
118
|
+
lines++;
|
|
119
|
+
c = range.end;
|
|
120
|
+
last = range.width + width - room;
|
|
121
|
+
room = width;
|
|
122
|
+
}
|
|
123
|
+
return lines + +(extra > 0 && last + extra > width);
|
|
124
|
+
};
|
|
125
|
+
const lineStart = (index) => {
|
|
126
|
+
let c = START;
|
|
127
|
+
for (let i = 0; i < index; i++) c = next(width, c).end;
|
|
128
|
+
return c;
|
|
129
|
+
};
|
|
130
|
+
const endCut = (line, room, e) => {
|
|
131
|
+
const from = offset(line);
|
|
132
|
+
const after = next(width, line);
|
|
133
|
+
const beyond = after && next(width, after.end);
|
|
134
|
+
let to = beyond ? offset(beyond.end) : length;
|
|
135
|
+
const hard = text.indexOf("\n", from);
|
|
136
|
+
if (hard >= 0 && hard < to) to = hard;
|
|
137
|
+
if (!p.r || p.r[0] !== from || p.r[1] !== to) p.r = [
|
|
138
|
+
from,
|
|
139
|
+
to,
|
|
140
|
+
prepareRange(text, from, to, font, o, p.tab)
|
|
141
|
+
];
|
|
142
|
+
return from + head(cutFrom(p.r[2], 0, room, {
|
|
143
|
+
...options,
|
|
144
|
+
at: "end",
|
|
145
|
+
ellipsis: e
|
|
146
|
+
}, true));
|
|
147
|
+
};
|
|
148
|
+
const tabbed = p.tab > 0 && text.includes(" ");
|
|
149
|
+
const nbsp = "\xA0".repeat(tabbed ? Math.ceil(p.tab / (widthOf(face, "\xA0") + spacing)) : 0);
|
|
150
|
+
const { whiteSpace, wordBreak } = p.q;
|
|
151
|
+
const layoutOptions = {
|
|
152
|
+
...whiteSpace ? { whiteSpace } : {},
|
|
153
|
+
...wordBreak ? { wordBreak } : {}
|
|
154
|
+
};
|
|
155
|
+
const edge = width - 1;
|
|
156
|
+
const plainWithin = (joined) => {
|
|
157
|
+
const plain = prepareWithSegments(joined, font, p.q);
|
|
158
|
+
let broken = false;
|
|
159
|
+
let lines = walkLineRanges(plain, width, (line) => void (broken ||= line.end.graphemeIndex > 0 || line.width > edge));
|
|
160
|
+
if (lines > n || !broken) return lines <= n;
|
|
161
|
+
lines = 0;
|
|
162
|
+
for (let c = START, line; lines <= n && (line = nextLine(plain, face, spacing, width, c, edge));) {
|
|
163
|
+
lines++;
|
|
164
|
+
c = line.end;
|
|
165
|
+
}
|
|
166
|
+
return lines <= n;
|
|
167
|
+
};
|
|
168
|
+
const richWithin = (items) => {
|
|
169
|
+
const prepared = prepareRichInline(items, layoutOptions);
|
|
170
|
+
const widest = (line) => {
|
|
171
|
+
let w = line.width;
|
|
172
|
+
for (const f of materializeRichInlineLineRange(prepared, line).fragments) {
|
|
173
|
+
const { text } = items[f.itemIndex];
|
|
174
|
+
if (f.text) w += Math.max(0, engineWidth(face, spacing, f.text, f.sourceEnd < text.length && line.end.graphemeIndex ? text.slice(f.sourceEnd) : "") - f.occupiedWidth);
|
|
175
|
+
}
|
|
176
|
+
return w;
|
|
177
|
+
};
|
|
178
|
+
let broken = false;
|
|
179
|
+
let c;
|
|
180
|
+
let lines = walkRichInlineLineRanges(prepared, width, (line) => void (broken ||= line.end.graphemeIndex > 0 || line.width > edge));
|
|
181
|
+
if (lines > n || !broken) return lines <= n;
|
|
182
|
+
for (lines = 0; lines <= n; lines++) {
|
|
183
|
+
const from = c;
|
|
184
|
+
const next = (room) => layoutNextRichInlineLineRange(prepared, room, from);
|
|
185
|
+
const line = settleLine(next(width), width, (from?.graphemeIndex ?? 0) > 0, edge, next, widest);
|
|
186
|
+
if (!line) break;
|
|
187
|
+
c = line.end;
|
|
188
|
+
}
|
|
189
|
+
return lines <= n;
|
|
190
|
+
};
|
|
191
|
+
const within = (before, rest) => {
|
|
192
|
+
if (typeof ellipsis === "string" && !after && !plainWithin(before + ellipsis + rest)) return false;
|
|
193
|
+
const items = [];
|
|
194
|
+
const words = (text) => {
|
|
195
|
+
if (text) items.push({
|
|
196
|
+
text,
|
|
197
|
+
font,
|
|
198
|
+
...spacing ? { letterSpacing: spacing } : {}
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
if (typeof ellipsis === "string") words(before + ellipsis + rest);
|
|
202
|
+
else {
|
|
203
|
+
words(before);
|
|
204
|
+
items.push({ width: ellipsis });
|
|
205
|
+
words(rest);
|
|
206
|
+
}
|
|
207
|
+
if (after) items.push({ width: after });
|
|
208
|
+
return richWithin(items);
|
|
209
|
+
};
|
|
210
|
+
const held = (text) => text.replaceAll(" ", nbsp);
|
|
211
|
+
const fits = (h, at) => {
|
|
212
|
+
const before = text.slice(0, h);
|
|
213
|
+
const rest = text.slice(at);
|
|
214
|
+
return within(before, rest) && (!tabbed || within(held(before), held(rest)));
|
|
215
|
+
};
|
|
216
|
+
const back = (h) => {
|
|
217
|
+
const graphemes = graphemesOf(text.slice(Math.max(0, h - 32), h));
|
|
218
|
+
return h - graphemes[graphemes.length - 1].segment.length;
|
|
219
|
+
};
|
|
220
|
+
const fitted = linesFrom(START, width, n, 0);
|
|
221
|
+
if (fitted <= n) {
|
|
222
|
+
const all = held(text);
|
|
223
|
+
if (!tabbed || plainWithin(all) && richWithin([{
|
|
224
|
+
text: all,
|
|
225
|
+
font,
|
|
226
|
+
...spacing ? { letterSpacing: spacing } : {}
|
|
227
|
+
}])) return pack(length, length);
|
|
228
|
+
}
|
|
229
|
+
const drawn = Math.min(n, fitted);
|
|
230
|
+
if (at === "end") {
|
|
231
|
+
let h = endCut(lineStart(drawn - 1), width - after, ellipsis);
|
|
232
|
+
while (tabbed && h > 0 && !fits(h, length)) h = back(h);
|
|
233
|
+
return pack(h, length);
|
|
234
|
+
}
|
|
235
|
+
const line = at === "start" ? 0 : Math.min(drawn - 1, Math.floor(n * ratio));
|
|
236
|
+
const start = lineStart(line);
|
|
237
|
+
const from = offset(start);
|
|
238
|
+
const share = at === "start" ? 0 : (n * ratio - line) * width;
|
|
239
|
+
let h = at === "start" ? 0 : endCut(start, share, 0);
|
|
240
|
+
let c;
|
|
241
|
+
const offsetOf = (i) => list[i] < 0 ? length : c.at[list[i]];
|
|
242
|
+
const cursorOf = (i) => list[i] < 0 ? START : {
|
|
243
|
+
segmentIndex: c.seg[list[i]],
|
|
244
|
+
graphemeIndex: c.g[list[i]]
|
|
245
|
+
};
|
|
246
|
+
const used = share + ew;
|
|
247
|
+
const starts = (h, wordsOnly) => {
|
|
248
|
+
c = startsOf(p, !wordsOnly);
|
|
249
|
+
if (list.length < c.n + 1) list = new Int32Array(2 * (c.n + 1));
|
|
250
|
+
listed = 0;
|
|
251
|
+
let lo = 0;
|
|
252
|
+
let hi = c.n;
|
|
253
|
+
while (lo < hi) {
|
|
254
|
+
const mid = lo + hi >> 1;
|
|
255
|
+
if (c.at[mid] > h) hi = mid;
|
|
256
|
+
else lo = mid + 1;
|
|
257
|
+
}
|
|
258
|
+
for (let j = lo; j < c.n; j++) if (!(trim && c.f[j])) list[listed++] = j;
|
|
259
|
+
list[listed++] = -1;
|
|
260
|
+
};
|
|
261
|
+
const first = () => {
|
|
262
|
+
let lo = 0;
|
|
263
|
+
let hi = listed - 1;
|
|
264
|
+
while (lo < hi) {
|
|
265
|
+
const mid = lo + hi >> 1;
|
|
266
|
+
if (linesFrom(cursorOf(mid), width - used, n - line) <= n - line) hi = mid;
|
|
267
|
+
else lo = mid + 1;
|
|
268
|
+
}
|
|
269
|
+
return lo;
|
|
270
|
+
};
|
|
271
|
+
let slipped = false;
|
|
272
|
+
const settle = (h) => {
|
|
273
|
+
const model = first();
|
|
274
|
+
let i = model;
|
|
275
|
+
while (i < listed - 1 && !fits(h, offsetOf(i))) i++;
|
|
276
|
+
if (at === "start") while (i > 0 && fits(h, offsetOf(i - 1))) i--;
|
|
277
|
+
slipped ||= i > model;
|
|
278
|
+
return i;
|
|
279
|
+
};
|
|
280
|
+
const tailOf = (h) => {
|
|
281
|
+
slipped = false;
|
|
282
|
+
starts(h, words !== false);
|
|
283
|
+
let i = settle(h);
|
|
284
|
+
if (i === listed - 1 && words !== false) {
|
|
285
|
+
starts(h, false);
|
|
286
|
+
i = settle(h);
|
|
287
|
+
}
|
|
288
|
+
return i < listed - 1 || fits(h, length) ? offsetOf(i) : -1;
|
|
289
|
+
};
|
|
290
|
+
for (;;) {
|
|
291
|
+
let t = tailOf(h);
|
|
292
|
+
if (t >= 0 || h <= from) {
|
|
293
|
+
if (t < 0) {
|
|
294
|
+
t = length;
|
|
295
|
+
while (h > 0 && !fits(h, length)) h = back(h);
|
|
296
|
+
}
|
|
297
|
+
if (at === "middle" && slipped) {
|
|
298
|
+
const ink = (a, b) => {
|
|
299
|
+
let count = 0;
|
|
300
|
+
for (let i = a; i < b; i++) if (text.charCodeAt(i) > 32) count++;
|
|
301
|
+
return count;
|
|
302
|
+
};
|
|
303
|
+
const words = startsOf(p, false);
|
|
304
|
+
let lo = 0;
|
|
305
|
+
let hi = words.n;
|
|
306
|
+
while (lo < hi) {
|
|
307
|
+
const mid = lo + hi >> 1;
|
|
308
|
+
if (words.at[mid] < h) lo = mid + 1;
|
|
309
|
+
else hi = mid;
|
|
310
|
+
}
|
|
311
|
+
while (lo > 0 && words.f[lo - 1]) lo--;
|
|
312
|
+
const floor = Math.min(from, lo ? words.at[lo - 1] : 0);
|
|
313
|
+
let best = h;
|
|
314
|
+
let bestTail = t;
|
|
315
|
+
for (let shorter = h; shorter > floor;) {
|
|
316
|
+
shorter = Math.max(floor, back(shorter));
|
|
317
|
+
const rest = tailOf(shorter);
|
|
318
|
+
if (rest >= 0 && ink(rest, bestTail) > ink(shorter, best)) {
|
|
319
|
+
best = shorter;
|
|
320
|
+
bestTail = rest;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
h = best;
|
|
324
|
+
t = bestTail;
|
|
325
|
+
}
|
|
326
|
+
return pack(h, t);
|
|
327
|
+
}
|
|
328
|
+
h = Math.max(from, back(h));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
//#endregion
|
|
332
|
+
export { prepareLines as n, roomAfter as r, clamp as t };
|
|
333
|
+
|
|
334
|
+
//# sourceMappingURL=lines-DOj2DQLl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lines-DOj2DQLl.js","names":[],"sources":["../src/lines.ts"],"sourcesContent":["import {\n clearCache,\n type LayoutCursor,\n layoutNextLineRange,\n materializeLineRange,\n type PrepareOptions as PretextOptions,\n prepareWithSegments,\n type PreparedTextWithSegments,\n walkLineRanges,\n} from \"@chenglou/pretext\";\nimport {\n layoutNextRichInlineLineRange,\n materializeRichInlineLineRange,\n prepareRichInline,\n type RichInlineBox,\n type RichInlineCursor,\n type RichInlineItem,\n type RichInlineLineRange,\n walkRichInlineLineRanges,\n} from \"@chenglou/pretext/rich-inline\";\nimport {\n type Cut,\n type CutOptions,\n cutFrom,\n head,\n pack,\n type Prepared,\n type Protect,\n prepareRange,\n} from \"./cut.ts\";\nimport { alsoForget, type FaceData, faceOf, widthOf } from \"./face.ts\";\n\n// pretext keeps its own widths by font, so a `forget` after a web font loads drops them\n// too: kept, they are the fallback's, and a clamp can fit text in fewer lines than it\n// paints in.\nalsoForget(clearCache);\n\n/** Text prepared for laying out in lines with pretext. */\nexport interface PreparedLines {\n /** The text as its white space mode paints it; every offset refers to this string. */\n readonly text: string;\n}\n\nexport interface PrepareLinesOptions {\n /** CSS `white-space`. Default \"normal\". */\n readonly whiteSpace?: \"normal\" | \"pre-wrap\";\n readonly wordBreak?: \"normal\" | \"keep-all\";\n /** CSS `letter-spacing` in px. */\n readonly letterSpacing?: number;\n /**\n * As in `prepare`, but a clamp counts only whole matches, on its last line, and keeps\n * each whole or drops it whole.\n */\n readonly protect?: Protect;\n}\n\nexport interface ClampOptions extends CutOptions {\n /** How many lines to keep. */\n readonly lines: number;\n /** Room in px to keep free after the text's end, for an element such as a \"more\" link. */\n readonly after?: number;\n}\n\ninterface Internal extends PreparedLines {\n readonly l: PreparedTextWithSegments;\n readonly font: string;\n /** Each segment's start offset, then the text's length. */\n readonly s: number[];\n readonly o: PrepareLinesOptions;\n /** pretext's options, as `prepareWithSegments` takes them. */\n readonly q: PretextOptions;\n /** A tab's width at most, in `pre-wrap`: eight spaces and half a `ch`. 0 in `normal`. */\n readonly tab: number;\n /** The last line region the core prepared: from, to, and its preparation. */\n r: readonly [number, number, Prepared] | undefined;\n /** Where a tail may start: word starts, then every grapheme start, each found once. */\n c: Starts | undefined;\n d: Starts | undefined;\n}\n\n/**\n * Tail starts in order: each one's offset, segment and grapheme index in the segment,\n * and 1 when a trimming cut never starts a tail there, in white space or at `,;:`. `n`\n * of them, then room to spare.\n */\ninterface Starts {\n readonly at: Int32Array;\n readonly seg: Int32Array;\n readonly g: Int32Array;\n readonly f: Uint8Array;\n readonly n: number;\n}\n\nconst START: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 };\nlet segmenter: Intl.Segmenter | undefined;\n// The candidate tail starts of the clamp in progress, as indices into its `Starts`, -1\n// for the text's end. Shared and grown as needed, so a clamp allocates no list.\nlet list = new Int32Array(64);\nlet listed = 0;\n\nconst graphemesOf = (text: string) => [...(segmenter ??= new Intl.Segmenter()).segment(text)];\n\n// A word longer than a line breaks between graphemes, and pretext measures the parts\n// grapheme by grapheme, where the engines measure a part as text: WebKit alone, Chromium\n// and Firefox within the whole word, its last grapheme kerned against the one after. A\n// part can come out over a px wider than pretext has it, so a line it fills to the edge\n// loses its last grapheme or box to the next line in an engine, and every line after\n// shifts (#8). This is text's widest width in an engine, measured both ways before\n// `after`, the text after it when the line ends inside a word. White space at the end\n// hangs, and a line with a tab is left to the tab checks. Widths are kept in the face,\n// so a clamp again at a width it has seen measures nothing.\nconst engineWidth = (face: FaceData, spacing: number, text: string, after = \"\") => {\n text = text.replace(/[ \\n]+$/, \"\");\n const next = graphemesOf(after.slice(0, 16))[0]?.segment;\n return text.includes(\"\\t\")\n ? 0\n : Math.max(widthOf(face, text), next ? widthOf(face, text + next) - widthOf(face, next) : 0) +\n spacing * graphemesOf(text).length;\n};\n\n// Lays `line`, from `next` in `room` px, out again in less room while it starts\n// (`inside`) or ends inside a word, or is wider than `edge`, and runs over by `widthOf`,\n// so it gives up a break at a time, as far as one grapheme: the strictest engine's line,\n// never ending after any engine's.\nconst settleLine = <L extends { readonly width: number; readonly end: LayoutCursor }>(\n line: L | null,\n room: number,\n inside: boolean,\n edge: number,\n next: (room: number) => L | null,\n widthOf: (line: L) => number,\n) => {\n for (\n let fit = room, tries = 0;\n line && (inside || line.end.graphemeIndex || line.width > edge) && tries < 8;\n tries++\n ) {\n const over = Math.max(line.width, widthOf(line)) - room;\n if (over <= 0) break;\n line = next((fit = Math.min(fit - over, line.width) - 0.001)) ?? line;\n }\n return line;\n};\n\n// pretext's next line of `p` from `c` in `room` px, settled as `settleLine` settles it\n// when it starts or ends inside a word, or is wider than `edge`.\nconst nextLine = (\n p: PreparedTextWithSegments,\n face: FaceData,\n spacing: number,\n room: number,\n c: LayoutCursor,\n edge = room,\n) => {\n const line = layoutNextLineRange(p, room, c);\n return line && (c.graphemeIndex || line.end.graphemeIndex || line.width > edge)\n ? settleLine(\n line,\n room,\n c.graphemeIndex > 0,\n edge,\n (room) => layoutNextLineRange(p, room, c),\n (line) =>\n engineWidth(\n face,\n spacing,\n materializeLineRange(p, line).text,\n graphemesOf(p.segments[line.end.segmentIndex] ?? \"\")[line.end.graphemeIndex]?.segment,\n ),\n )\n : line;\n};\n\n// Word starts are segment starts, which pretext already gives, so only a grapheme table\n// meets the segmenter, and only once a clamp needs one: without word preference, or\n// when no word start fits.\nconst startsOf = (p: Internal, graphemes: boolean): Starts => {\n const known = graphemes ? p.d : p.c;\n if (known) return known;\n const { l, s, text } = p;\n const size = (graphemes ? text.length : l.segments.length) + 1;\n const at = new Int32Array(size);\n const seg = new Int32Array(size);\n const g = new Int32Array(size);\n const f = new Uint8Array(size);\n let n = 0;\n for (let i = 0; i < l.segments.length; i++) {\n const segment = l.segments[i]!;\n const space = /^\\s+$/.test(segment);\n let k = 0;\n for (const grapheme of graphemes ? graphemesOf(segment) : [{ index: 0 }]) {\n const offset = s[i]! + grapheme.index;\n const code = text.charCodeAt(offset);\n at[n] = offset;\n seg[n] = i;\n g[n] = k++;\n f[n++] = +(space || code === 44 || code === 58 || code === 59);\n }\n }\n const starts = { at, seg, g, f, n };\n return graphemes ? (p.d = starts) : (p.c = starts);\n};\n\nexport const prepareLines = (\n text: string,\n font: string,\n options: PrepareLinesOptions = {},\n): PreparedLines => {\n const q: PretextOptions = {\n ...(options.whiteSpace ? { whiteSpace: options.whiteSpace } : {}),\n ...(options.wordBreak ? { wordBreak: options.wordBreak } : {}),\n ...(options.letterSpacing ? { letterSpacing: options.letterSpacing } : {}),\n };\n const l = prepareWithSegments(text, font, q);\n // pretext collapses white space as the core does, or keeps it in `pre-wrap` (with CRLF\n // as one break), and its segments join to the text every offset refers to.\n let joined = \"\";\n const s = [0];\n for (const segment of l.segments) s.push((joined += segment).length);\n // A tab advances to the next stop, eight spaces apart, unless that is under half a `ch`\n // away, when Firefox and WebKit take the stop after, as CSS Text says: so up to eight\n // spaces and half a \"0\".\n const face = faceOf(font);\n const tab =\n options.whiteSpace === \"pre-wrap\"\n ? 8 * (widthOf(face, \" \") + (options.letterSpacing ?? 0)) + widthOf(face, \"0\") / 2\n : 0;\n const p: Internal = {\n text: joined,\n l,\n font,\n s,\n o: options,\n q,\n tab,\n r: undefined,\n c: undefined,\n d: undefined,\n };\n return p;\n};\n\n/** The room in px after the last line of the whole text, where a \"less\" link would go. */\nexport const roomAfter = (lines: PreparedLines, width: number): number => {\n let last = 0;\n walkLineRanges((lines as Internal).l, width, (line) => (last = line.width));\n return width - last;\n};\n\n/**\n * Cuts text to `options.lines` lines of `width` px, with the ellipsis at the end of the\n * last line, the start of the first, or inside a middle line (`ratio` of the way through).\n */\nexport const clamp = (lines: PreparedLines, width: number, options: ClampOptions): Cut => {\n const p = lines as Internal;\n const { text, l, font, s, o } = p;\n const { at = \"end\", ratio = 0.5, words = true, trim = true, ellipsis = \"…\", after = 0 } = options;\n const n = Math.max(1, options.lines);\n const length = text.length;\n const spacing = o.letterSpacing ?? 0;\n const face = faceOf(font);\n const ew =\n typeof ellipsis === \"number\" ? ellipsis : widthOf(face, ellipsis) + spacing * ellipsis.length;\n const next = (room: number, c: LayoutCursor) => nextLine(l, face, spacing, room, c);\n const offset = (c: LayoutCursor) =>\n s[c.segmentIndex]! +\n (c.graphemeIndex ? graphemesOf(l.segments[c.segmentIndex]!)[c.graphemeIndex]!.index : 0);\n // How many lines the text from `from` takes, the first `first` px wide, counting no\n // further than one past `limit`, and one more when `after` doesn't fit beside the\n // last. Arithmetic over pretext's prepared widths, but for a break inside a word.\n const linesFrom = (from: LayoutCursor, first: number, limit: number, extra = after) => {\n let lines = 0;\n let c: LayoutCursor = from;\n let room = first;\n let last = 0;\n while (lines <= limit) {\n const range = next(room, c);\n if (range === null) break;\n lines++;\n c = range.end;\n last = range.width + width - room;\n room = width;\n }\n return lines + +(extra > 0 && last + extra > width);\n };\n // The start of line `index` (from 0); the model has more lines than that.\n const lineStart = (index: number) => {\n let c = START;\n for (let i = 0; i < index; i++) c = next(width, c)!.end;\n return c;\n };\n // The core's end cut of the line starting at `line`, in `room` px. A cut keeps at most\n // a line, so the core prepares only the region from there to the end of the line\n // after, or to a hard break, and keeps the last one for the next cut.\n const endCut = (line: LayoutCursor, room: number, e: string | number) => {\n const from = offset(line);\n const after = next(width, line);\n const beyond = after && next(width, after.end);\n let to = beyond ? offset(beyond.end) : length;\n const hard = text.indexOf(\"\\n\", from);\n if (hard >= 0 && hard < to) to = hard;\n if (!p.r || p.r[0] !== from || p.r[1] !== to) {\n p.r = [from, to, prepareRange(text, from, to, font, o, p.tab)];\n }\n // The text goes on past the region, so the ellipsis always needs its room.\n return from + head(cutFrom(p.r[2], 0, room, { ...options, at: \"end\", ellipsis: e }, true));\n };\n\n // pretext's plain and rich-inline layouts can each be the one that matches an engine,\n // so the final check is the stricter of them. It lays out at the model's width: a\n // line that fit the model and not a narrower check shifted the tail at some widths\n // and not the next. The last line's cut keeps its own slack in the core.\n // Engines lay tabs out three ways, and pretext predicts none of them exactly under\n // letter spacing: Chrome and WebKit let a tab hang past the line's end, Firefox keeps\n // it with the word before and wraps the two together. So text with tabs is also\n // checked with each tab as no-break spaces, as many as reach the widest a tab can\n // take, held to the text on both sides.\n const tabbed = p.tab > 0 && text.includes(\"\\t\");\n const nbsp = \"\\u00a0\".repeat(tabbed ? Math.ceil(p.tab / (widthOf(face, \"\\u00a0\") + spacing)) : 0);\n const { whiteSpace, wordBreak } = p.q;\n const layoutOptions = {\n ...(whiteSpace ? { whiteSpace } : {}),\n ...(wordBreak ? { wordBreak } : {}),\n };\n // The checks also settle a line within a px of the edge: pretext can sum a line's parts\n // narrower than an engine lays them out together, as Firefox puts \"文 😀 ~/…\" at 72.03 px\n // where pretext has 71.45 (#10).\n const edge = width - 1;\n // Whether the plain layout of `joined` takes n lines at most. Its lines are laid out\n // again one by one, settled, only when one breaks inside a word or nears the edge.\n const plainWithin = (joined: string) => {\n const plain = prepareWithSegments(joined, font, p.q);\n let broken = false;\n let lines = walkLineRanges(\n plain,\n width,\n (line) => void (broken ||= line.end.graphemeIndex > 0 || line.width > edge),\n );\n if (lines > n || !broken) return lines <= n;\n lines = 0;\n for (\n let c = START, line;\n lines <= n && (line = nextLine(plain, face, spacing, width, c, edge));\n ) {\n lines++;\n c = line.end;\n }\n return lines <= n;\n };\n // Whether the rich-inline layout of `items` takes n lines at most, each line settled\n // as `nextLine` settles it.\n const richWithin = (items: Array<RichInlineItem | RichInlineBox>) => {\n const prepared = prepareRichInline(items, layoutOptions);\n // A text part's width is its widest in an engine, or pretext's when that is wider.\n const widest = (line: RichInlineLineRange) => {\n let w = line.width;\n for (const f of materializeRichInlineLineRange(prepared, line).fragments) {\n const { text } = items[f.itemIndex] as RichInlineItem;\n if (f.text) {\n w += Math.max(\n 0,\n engineWidth(\n face,\n spacing,\n f.text,\n f.sourceEnd < text.length && line.end.graphemeIndex ? text.slice(f.sourceEnd) : \"\",\n ) - f.occupiedWidth,\n );\n }\n }\n return w;\n };\n let broken = false;\n let c: RichInlineCursor | undefined;\n let lines = walkRichInlineLineRanges(\n prepared,\n width,\n (line) => void (broken ||= line.end.graphemeIndex > 0 || line.width > edge),\n );\n if (lines > n || !broken) return lines <= n;\n for (lines = 0; lines <= n; lines++) {\n const from = c;\n const next = (room: number) => layoutNextRichInlineLineRange(prepared, room, from);\n const line = settleLine(\n next(width),\n width,\n (from?.graphemeIndex ?? 0) > 0,\n edge,\n next,\n widest,\n );\n if (!line) break;\n c = line.end;\n }\n return lines <= n;\n };\n // Whether text, an ellipsis and text, then `after`'s room, lay out in n lines: the\n // ellipsis given as a width and the room after as boxes of their size.\n const within = (before: string, rest: string) => {\n if (typeof ellipsis === \"string\" && !after && !plainWithin(before + ellipsis + rest)) {\n return false;\n }\n const items: Array<RichInlineItem | RichInlineBox> = [];\n const words = (text: string) => {\n if (text) items.push({ text, font, ...(spacing ? { letterSpacing: spacing } : {}) });\n };\n // A text ellipsis is one run with the text, as it renders; apart, the layout would\n // find a break between them that the joined string doesn't have.\n if (typeof ellipsis === \"string\") words(before + ellipsis + rest);\n else {\n words(before);\n items.push({ width: ellipsis });\n words(rest);\n }\n if (after) items.push({ width: after });\n return richWithin(items);\n };\n const held = (text: string) => text.replaceAll(\"\\t\", nbsp);\n // Whether the cut keeping [0, h) and [at, length) really lays out in n lines. The\n // model lays the tail out apart from the head, and the joined line can wrap\n // differently: a tail that begins mid-word can't break where a line start could, and\n // an ellipsis can end the line before.\n const fits = (h: number, at: number) => {\n const before = text.slice(0, h);\n const rest = text.slice(at);\n return within(before, rest) && (!tabbed || within(held(before), held(rest)));\n };\n // The grapheme start before `h`.\n const back = (h: number) => {\n const graphemes = graphemesOf(text.slice(Math.max(0, h - 32), h));\n return h - graphemes[graphemes.length - 1]!.segment.length;\n };\n\n // Text that fits shows whole, with nothing after it, so `after` takes no room here.\n // Tabbed text must also fit with each tab at its widest, which the model doesn't know.\n const fitted = linesFrom(START, width, n, 0);\n if (fitted <= n) {\n const all = held(text);\n if (\n !tabbed ||\n (plainWithin(all) &&\n richWithin([{ text: all, font, ...(spacing ? { letterSpacing: spacing } : {}) }]))\n ) {\n return pack(length, length);\n }\n }\n // The model's lines, at most n. Tabbed text the model fits in fewer can still run\n // over with its tabs at their widest, so the cut works within the lines it has.\n const drawn = Math.min(n, fitted);\n\n if (at === \"end\") {\n let h = endCut(lineStart(drawn - 1), width - after, ellipsis);\n // The core cuts the last line with each tab at its widest, but the lines before come\n // from the model, so tabbed text is checked on the real layout too, as start and\n // middle cuts are.\n while (tabbed && h > 0 && !fits(h, length)) h = back(h);\n return pack(h, length);\n }\n\n // The text before the tail stays on lines [0, line) and `used` px of line `line`.\n const line = at === \"start\" ? 0 : Math.min(drawn - 1, Math.floor(n * ratio));\n const start = lineStart(line);\n const from = offset(start);\n // The head's share of its line. The tail gets the rest of that line whatever the head\n // keeps of its share, so neither end takes the other's slack and, as the width moves,\n // each end only ever grows or only ever shrinks.\n const share = at === \"start\" ? 0 : (n * ratio - line) * width;\n let h = at === \"start\" ? 0 : endCut(start, share, 0);\n // The table the candidates index into, word starts or grapheme starts.\n let c: Starts;\n // Candidate `i`'s offset and line cursor; -1 is the text's end.\n const offsetOf = (i: number) => (list[i]! < 0 ? length : c.at[list[i]!]!);\n const cursorOf = (i: number): LayoutCursor =>\n list[i]! < 0 ? START : { segmentIndex: c.seg[list[i]!]!, graphemeIndex: c.g[list[i]!]! };\n const used = share + ew;\n // Where the tail may start, after head `h` (cutting nothing is no cut): word starts,\n // or every grapheme start without word preference. When trimming, never white space\n // nor `,;:`. The text's end always closes the list.\n const starts = (h: number, wordsOnly: boolean) => {\n c = startsOf(p, !wordsOnly);\n if (list.length < c.n + 1) list = new Int32Array(2 * (c.n + 1));\n listed = 0;\n let lo = 0;\n let hi = c.n;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (c.at[mid]! > h) hi = mid;\n else lo = mid + 1;\n }\n for (let j = lo; j < c.n; j++) if (!(trim && c.f[j])) list[listed++] = j;\n list[listed++] = -1;\n };\n // The first start whose tail fits the model: its lines, the first beside the head\n // and the ellipsis, end by line n. A later start never takes more lines.\n const first = () => {\n let lo = 0;\n let hi = listed - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (linesFrom(cursorOf(mid), width - used, n - line) <= n - line) hi = mid;\n else lo = mid + 1;\n }\n return lo;\n };\n // Whether the real layout refused the model's start for the last tail found.\n let slipped = false;\n // The cut settles on the real layout: later while it runs over, and, with no head\n // whose room it would take, earlier while one more start still fits.\n const settle = (h: number) => {\n const model = first();\n let i = model;\n while (i < listed - 1 && !fits(h, offsetOf(i))) i++;\n if (at === \"start\") while (i > 0 && fits(h, offsetOf(i - 1))) i--;\n slipped ||= i > model;\n return i;\n };\n // The earliest tail start that really fits after head `h`, or -1 when none does.\n const tailOf = (h: number) => {\n slipped = false;\n starts(h, words !== false);\n let i = settle(h);\n // With no word start that really fits, the tail starts between graphemes, as the\n // core falls back. Only after settling: the model can pass a word start that the\n // joined layout refuses, when the ellipsis takes a line of its own.\n if (i === listed - 1 && words !== false) {\n starts(h, false);\n i = settle(h);\n }\n return i < listed - 1 || fits(h, length) ? offsetOf(i) : -1;\n };\n for (;;) {\n let t = tailOf(h);\n if (t >= 0 || h <= from) {\n // No tail fits even with the head back at its line's start, so the head shows\n // alone, and settles on the real layout like any other cut: an engine can break\n // its lines elsewhere than the model, as Firefox breaks after a slash and not before\n // an ellipsis, and wrap even the head alone past them (#7).\n if (t < 0) {\n t = length;\n while (h > 0 && !fits(h, length)) h = back(h);\n }\n // When the real layout refused the model's start, the head's last word and the tail\n // wrap together: one more letter of head can push the ellipsis or the tail's first\n // word to the next line and cost the tail more than it gave. Then every\n // shorter head back to the start of its line or of that word, whichever is earlier,\n // is tried, and the one that keeps the most text in all wins, the longest on a tie,\n // so a wider box doesn't show less. Text is counted without white space, which\n // shows as nothing.\n if (at === \"middle\" && slipped) {\n const ink = (a: number, b: number) => {\n let count = 0;\n for (let i = a; i < b; i++) if (text.charCodeAt(i) > 32) count++;\n return count;\n };\n const words = startsOf(p, false);\n let lo = 0;\n let hi = words.n;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (words.at[mid]! < h) lo = mid + 1;\n else hi = mid;\n }\n // Spaces are segments too: the word is the last start that isn't one.\n while (lo > 0 && words.f[lo - 1]) lo--;\n const floor = Math.min(from, lo ? words.at[lo - 1]! : 0);\n let best = h;\n let bestTail = t;\n for (let shorter = h; shorter > floor;) {\n shorter = Math.max(floor, back(shorter));\n const rest = tailOf(shorter);\n if (rest >= 0 && ink(rest, bestTail) > ink(shorter, best)) {\n best = shorter;\n bestTail = rest;\n }\n }\n h = best;\n t = bestTail;\n }\n return pack(h, t);\n }\n // With no tail at all still running over, as when the room after leaves less than\n // the head took, the head gives up a grapheme and the tail is found again.\n h = Math.max(from, back(h));\n }\n};\n"],"mappings":";;;;;AAmCA,WAAW,UAAU;AA0DrB,IAAM,QAAsB;CAAE,cAAc;CAAG,eAAe;AAAE;AAChE,IAAI;AAGJ,IAAI,uBAAO,IAAI,WAAW,EAAE;AAC5B,IAAI,SAAS;AAEb,IAAM,eAAe,SAAiB,CAAC,IAAI,cAAc,IAAI,KAAK,UAAU,EAAA,CAAG,QAAQ,IAAI,CAAC;AAW5F,IAAM,eAAe,MAAgB,SAAiB,MAAc,QAAQ,OAAO;CACjF,OAAO,KAAK,QAAQ,WAAW,EAAE;CACjC,MAAM,OAAO,YAAY,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;CACjD,OAAO,KAAK,SAAS,GAAI,IACrB,IACA,KAAK,IAAI,QAAQ,MAAM,IAAI,GAAG,OAAO,QAAQ,MAAM,OAAO,IAAI,IAAI,QAAQ,MAAM,IAAI,IAAI,CAAC,IACvF,UAAU,YAAY,IAAI,CAAC,CAAC;AACpC;AAMA,IAAM,cACJ,MACA,MACA,QACA,MACA,MACA,YACG;CACH,KACE,IAAI,MAAM,MAAM,QAAQ,GACxB,SAAS,UAAU,KAAK,IAAI,iBAAiB,KAAK,QAAQ,SAAS,QAAQ,GAC3E,SACA;EACA,MAAM,OAAO,KAAK,IAAI,KAAK,OAAO,QAAQ,IAAI,CAAC,IAAI;EACnD,IAAI,QAAQ,GAAG;EACf,OAAO,KAAM,MAAM,KAAK,IAAI,MAAM,MAAM,KAAK,KAAK,IAAI,IAAM,KAAK;CACnE;CACA,OAAO;AACT;AAIA,IAAM,YACJ,GACA,MACA,SACA,MACA,GACA,OAAO,SACJ;CACH,MAAM,OAAO,oBAAoB,GAAG,MAAM,CAAC;CAC3C,OAAO,SAAS,EAAE,iBAAiB,KAAK,IAAI,iBAAiB,KAAK,QAAQ,QACtE,WACE,MACA,MACA,EAAE,gBAAgB,GAClB,OACC,SAAS,oBAAoB,GAAG,MAAM,CAAC,IACvC,SACC,YACE,MACA,SACA,qBAAqB,GAAG,IAAI,CAAC,CAAC,MAC9B,YAAY,EAAE,SAAS,KAAK,IAAI,iBAAiB,EAAE,CAAC,CAAC,KAAK,IAAI,cAAc,EAAE,OAChF,CACJ,IACA;AACN;AAKA,IAAM,YAAY,GAAa,cAA+B;CAC5D,MAAM,QAAQ,YAAY,EAAE,IAAI,EAAE;CAClC,IAAI,OAAO,OAAO;CAClB,MAAM,EAAE,GAAG,GAAG,SAAS;CACvB,MAAM,QAAQ,YAAY,KAAK,SAAS,EAAE,SAAS,UAAU;CAC7D,MAAM,KAAK,IAAI,WAAW,IAAI;CAC9B,MAAM,MAAM,IAAI,WAAW,IAAI;CAC/B,MAAM,IAAI,IAAI,WAAW,IAAI;CAC7B,MAAM,IAAI,IAAI,WAAW,IAAI;CAC7B,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,SAAS,QAAQ,KAAK;EAC1C,MAAM,UAAU,EAAE,SAAS;EAC3B,MAAM,QAAQ,QAAQ,KAAK,OAAO;EAClC,IAAI,IAAI;EACR,KAAK,MAAM,YAAY,YAAY,YAAY,OAAO,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG;GACxE,MAAM,SAAS,EAAE,KAAM,SAAS;GAChC,MAAM,OAAO,KAAK,WAAW,MAAM;GACnC,GAAG,KAAK;GACR,IAAI,KAAK;GACT,EAAE,KAAK;GACP,EAAE,OAAO,EAAE,SAAS,SAAS,MAAM,SAAS,MAAM,SAAS;EAC7D;CACF;CACA,MAAM,SAAS;EAAE;EAAI;EAAK;EAAG;EAAG;CAAE;CAClC,OAAO,YAAa,EAAE,IAAI,SAAW,EAAE,IAAI;AAC7C;AAEA,IAAa,gBACX,MACA,MACA,UAA+B,CAAC,MACd;CAClB,MAAM,IAAoB;EACxB,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;EAC/D,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;EAC5D,GAAI,QAAQ,gBAAgB,EAAE,eAAe,QAAQ,cAAc,IAAI,CAAC;CAC1E;CACA,MAAM,IAAI,oBAAoB,MAAM,MAAM,CAAC;CAG3C,IAAI,SAAS;CACb,MAAM,IAAI,CAAC,CAAC;CACZ,KAAK,MAAM,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,QAAA,CAAS,MAAM;CAInE,MAAM,OAAO,OAAO,IAAI;CACxB,MAAM,MACJ,QAAQ,eAAe,aACnB,KAAK,QAAQ,MAAM,GAAG,KAAK,QAAQ,iBAAiB,MAAM,QAAQ,MAAM,GAAG,IAAI,IAC/E;CAaN,OAAO;EAXL,MAAM;EACN;EACA;EACA;EACA,GAAG;EACH;EACA;EACA,GAAG,KAAA;EACH,GAAG,KAAA;EACH,GAAG,KAAA;CAEE;AACT;;AAGA,IAAa,aAAa,OAAsB,UAA0B;CACxE,IAAI,OAAO;CACX,eAAgB,MAAmB,GAAG,QAAQ,SAAU,OAAO,KAAK,KAAM;CAC1E,OAAO,QAAQ;AACjB;;;;;AAMA,IAAa,SAAS,OAAsB,OAAe,YAA+B;CACxF,MAAM,IAAI;CACV,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM;CAChC,MAAM,EAAE,KAAK,OAAO,QAAQ,IAAK,QAAQ,MAAM,OAAO,MAAM,WAAW,KAAK,QAAQ,MAAM;CAC1F,MAAM,IAAI,KAAK,IAAI,GAAG,QAAQ,KAAK;CACnC,MAAM,SAAS,KAAK;CACpB,MAAM,UAAU,EAAE,iBAAiB;CACnC,MAAM,OAAO,OAAO,IAAI;CACxB,MAAM,KACJ,OAAO,aAAa,WAAW,WAAW,QAAQ,MAAM,QAAQ,IAAI,UAAU,SAAS;CACzF,MAAM,QAAQ,MAAc,MAAoB,SAAS,GAAG,MAAM,SAAS,MAAM,CAAC;CAClF,MAAM,UAAU,MACd,EAAE,EAAE,iBACH,EAAE,gBAAgB,YAAY,EAAE,SAAS,EAAE,aAAc,CAAC,CAAC,EAAE,cAAc,CAAE,QAAQ;CAIxF,MAAM,aAAa,MAAoB,OAAe,OAAe,QAAQ,UAAU;EACrF,IAAI,QAAQ;EACZ,IAAI,IAAkB;EACtB,IAAI,OAAO;EACX,IAAI,OAAO;EACX,OAAO,SAAS,OAAO;GACrB,MAAM,QAAQ,KAAK,MAAM,CAAC;GAC1B,IAAI,UAAU,MAAM;GACpB;GACA,IAAI,MAAM;GACV,OAAO,MAAM,QAAQ,QAAQ;GAC7B,OAAO;EACT;EACA,OAAO,QAAQ,EAAE,QAAQ,KAAK,OAAO,QAAQ;CAC/C;CAEA,MAAM,aAAa,UAAkB;EACnC,IAAI,IAAI;EACR,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,CAAE;EACpD,OAAO;CACT;CAIA,MAAM,UAAU,MAAoB,MAAc,MAAuB;EACvE,MAAM,OAAO,OAAO,IAAI;EACxB,MAAM,QAAQ,KAAK,OAAO,IAAI;EAC9B,MAAM,SAAS,SAAS,KAAK,OAAO,MAAM,GAAG;EAC7C,IAAI,KAAK,SAAS,OAAO,OAAO,GAAG,IAAI;EACvC,MAAM,OAAO,KAAK,QAAQ,MAAM,IAAI;EACpC,IAAI,QAAQ,KAAK,OAAO,IAAI,KAAK;EACjC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,QAAQ,EAAE,EAAE,OAAO,IACxC,EAAE,IAAI;GAAC;GAAM;GAAI,aAAa,MAAM,MAAM,IAAI,MAAM,GAAG,EAAE,GAAG;EAAC;EAG/D,OAAO,OAAO,KAAK,QAAQ,EAAE,EAAE,IAAI,GAAG,MAAM;GAAE,GAAG;GAAS,IAAI;GAAO,UAAU;EAAE,GAAG,IAAI,CAAC;CAC3F;CAWA,MAAM,SAAS,EAAE,MAAM,KAAK,KAAK,SAAS,GAAI;CAC9C,MAAM,OAAO,OAAS,OAAO,SAAS,KAAK,KAAK,EAAE,OAAO,QAAQ,MAAM,MAAQ,IAAI,QAAQ,IAAI,CAAC;CAChG,MAAM,EAAE,YAAY,cAAc,EAAE;CACpC,MAAM,gBAAgB;EACpB,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;CACnC;CAIA,MAAM,OAAO,QAAQ;CAGrB,MAAM,eAAe,WAAmB;EACtC,MAAM,QAAQ,oBAAoB,QAAQ,MAAM,EAAE,CAAC;EACnD,IAAI,SAAS;EACb,IAAI,QAAQ,eACV,OACA,QACC,SAAS,MAAM,WAAW,KAAK,IAAI,gBAAgB,KAAK,KAAK,QAAQ,KACxE;EACA,IAAI,QAAQ,KAAK,CAAC,QAAQ,OAAO,SAAS;EAC1C,QAAQ;EACR,KACE,IAAI,IAAI,OAAO,MACf,SAAS,MAAM,OAAO,SAAS,OAAO,MAAM,SAAS,OAAO,GAAG,IAAI,KACnE;GACA;GACA,IAAI,KAAK;EACX;EACA,OAAO,SAAS;CAClB;CAGA,MAAM,cAAc,UAAiD;EACnE,MAAM,WAAW,kBAAkB,OAAO,aAAa;EAEvD,MAAM,UAAU,SAA8B;GAC5C,IAAI,IAAI,KAAK;GACb,KAAK,MAAM,KAAK,+BAA+B,UAAU,IAAI,CAAC,CAAC,WAAW;IACxE,MAAM,EAAE,SAAS,MAAM,EAAE;IACzB,IAAI,EAAE,MACJ,KAAK,KAAK,IACR,GACA,YACE,MACA,SACA,EAAE,MACF,EAAE,YAAY,KAAK,UAAU,KAAK,IAAI,gBAAgB,KAAK,MAAM,EAAE,SAAS,IAAI,EAClF,IAAI,EAAE,aACR;GAEJ;GACA,OAAO;EACT;EACA,IAAI,SAAS;EACb,IAAI;EACJ,IAAI,QAAQ,yBACV,UACA,QACC,SAAS,MAAM,WAAW,KAAK,IAAI,gBAAgB,KAAK,KAAK,QAAQ,KACxE;EACA,IAAI,QAAQ,KAAK,CAAC,QAAQ,OAAO,SAAS;EAC1C,KAAK,QAAQ,GAAG,SAAS,GAAG,SAAS;GACnC,MAAM,OAAO;GACb,MAAM,QAAQ,SAAiB,8BAA8B,UAAU,MAAM,IAAI;GACjF,MAAM,OAAO,WACX,KAAK,KAAK,GACV,QACC,MAAM,iBAAiB,KAAK,GAC7B,MACA,MACA,MACF;GACA,IAAI,CAAC,MAAM;GACX,IAAI,KAAK;EACX;EACA,OAAO,SAAS;CAClB;CAGA,MAAM,UAAU,QAAgB,SAAiB;EAC/C,IAAI,OAAO,aAAa,YAAY,CAAC,SAAS,CAAC,YAAY,SAAS,WAAW,IAAI,GACjF,OAAO;EAET,MAAM,QAA+C,CAAC;EACtD,MAAM,SAAS,SAAiB;GAC9B,IAAI,MAAM,MAAM,KAAK;IAAE;IAAM;IAAM,GAAI,UAAU,EAAE,eAAe,QAAQ,IAAI,CAAC;GAAG,CAAC;EACrF;EAGA,IAAI,OAAO,aAAa,UAAU,MAAM,SAAS,WAAW,IAAI;OAC3D;GACH,MAAM,MAAM;GACZ,MAAM,KAAK,EAAE,OAAO,SAAS,CAAC;GAC9B,MAAM,IAAI;EACZ;EACA,IAAI,OAAO,MAAM,KAAK,EAAE,OAAO,MAAM,CAAC;EACtC,OAAO,WAAW,KAAK;CACzB;CACA,MAAM,QAAQ,SAAiB,KAAK,WAAW,KAAM,IAAI;CAKzD,MAAM,QAAQ,GAAW,OAAe;EACtC,MAAM,SAAS,KAAK,MAAM,GAAG,CAAC;EAC9B,MAAM,OAAO,KAAK,MAAM,EAAE;EAC1B,OAAO,OAAO,QAAQ,IAAI,MAAM,CAAC,UAAU,OAAO,KAAK,MAAM,GAAG,KAAK,IAAI,CAAC;CAC5E;CAEA,MAAM,QAAQ,MAAc;EAC1B,MAAM,YAAY,YAAY,KAAK,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;EAChE,OAAO,IAAI,UAAU,UAAU,SAAS,EAAE,CAAE,QAAQ;CACtD;CAIA,MAAM,SAAS,UAAU,OAAO,OAAO,GAAG,CAAC;CAC3C,IAAI,UAAU,GAAG;EACf,MAAM,MAAM,KAAK,IAAI;EACrB,IACE,CAAC,UACA,YAAY,GAAG,KACd,WAAW,CAAC;GAAE,MAAM;GAAK;GAAM,GAAI,UAAU,EAAE,eAAe,QAAQ,IAAI,CAAC;EAAG,CAAC,CAAC,GAElF,OAAO,KAAK,QAAQ,MAAM;CAE9B;CAGA,MAAM,QAAQ,KAAK,IAAI,GAAG,MAAM;CAEhC,IAAI,OAAO,OAAO;EAChB,IAAI,IAAI,OAAO,UAAU,QAAQ,CAAC,GAAG,QAAQ,OAAO,QAAQ;EAI5D,OAAO,UAAU,IAAI,KAAK,CAAC,KAAK,GAAG,MAAM,GAAG,IAAI,KAAK,CAAC;EACtD,OAAO,KAAK,GAAG,MAAM;CACvB;CAGA,MAAM,OAAO,OAAO,UAAU,IAAI,KAAK,IAAI,QAAQ,GAAG,KAAK,MAAM,IAAI,KAAK,CAAC;CAC3E,MAAM,QAAQ,UAAU,IAAI;CAC5B,MAAM,OAAO,OAAO,KAAK;CAIzB,MAAM,QAAQ,OAAO,UAAU,KAAK,IAAI,QAAQ,QAAQ;CACxD,IAAI,IAAI,OAAO,UAAU,IAAI,OAAO,OAAO,OAAO,CAAC;CAEnD,IAAI;CAEJ,MAAM,YAAY,MAAe,KAAK,KAAM,IAAI,SAAS,EAAE,GAAG,KAAK;CACnE,MAAM,YAAY,MAChB,KAAK,KAAM,IAAI,QAAQ;EAAE,cAAc,EAAE,IAAI,KAAK;EAAO,eAAe,EAAE,EAAE,KAAK;CAAM;CACzF,MAAM,OAAO,QAAQ;CAIrB,MAAM,UAAU,GAAW,cAAuB;EAChD,IAAI,SAAS,GAAG,CAAC,SAAS;EAC1B,IAAI,KAAK,SAAS,EAAE,IAAI,GAAG,OAAO,IAAI,WAAW,KAAK,EAAE,IAAI,EAAE;EAC9D,SAAS;EACT,IAAI,KAAK;EACT,IAAI,KAAK,EAAE;EACX,OAAO,KAAK,IAAI;GACd,MAAM,MAAO,KAAK,MAAO;GACzB,IAAI,EAAE,GAAG,OAAQ,GAAG,KAAK;QACpB,KAAK,MAAM;EAClB;EACA,KAAK,IAAI,IAAI,IAAI,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,KAAK,YAAY;EACvE,KAAK,YAAY;CACnB;CAGA,MAAM,cAAc;EAClB,IAAI,KAAK;EACT,IAAI,KAAK,SAAS;EAClB,OAAO,KAAK,IAAI;GACd,MAAM,MAAO,KAAK,MAAO;GACzB,IAAI,UAAU,SAAS,GAAG,GAAG,QAAQ,MAAM,IAAI,IAAI,KAAK,IAAI,MAAM,KAAK;QAClE,KAAK,MAAM;EAClB;EACA,OAAO;CACT;CAEA,IAAI,UAAU;CAGd,MAAM,UAAU,MAAc;EAC5B,MAAM,QAAQ,MAAM;EACpB,IAAI,IAAI;EACR,OAAO,IAAI,SAAS,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,GAAG;EAChD,IAAI,OAAO,SAAS,OAAO,IAAI,KAAK,KAAK,GAAG,SAAS,IAAI,CAAC,CAAC,GAAG;EAC9D,YAAY,IAAI;EAChB,OAAO;CACT;CAEA,MAAM,UAAU,MAAc;EAC5B,UAAU;EACV,OAAO,GAAG,UAAU,KAAK;EACzB,IAAI,IAAI,OAAO,CAAC;EAIhB,IAAI,MAAM,SAAS,KAAK,UAAU,OAAO;GACvC,OAAO,GAAG,KAAK;GACf,IAAI,OAAO,CAAC;EACd;EACA,OAAO,IAAI,SAAS,KAAK,KAAK,GAAG,MAAM,IAAI,SAAS,CAAC,IAAI;CAC3D;CACA,SAAS;EACP,IAAI,IAAI,OAAO,CAAC;EAChB,IAAI,KAAK,KAAK,KAAK,MAAM;GAKvB,IAAI,IAAI,GAAG;IACT,IAAI;IACJ,OAAO,IAAI,KAAK,CAAC,KAAK,GAAG,MAAM,GAAG,IAAI,KAAK,CAAC;GAC9C;GAQA,IAAI,OAAO,YAAY,SAAS;IAC9B,MAAM,OAAO,GAAW,MAAc;KACpC,IAAI,QAAQ;KACZ,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,KAAK,WAAW,CAAC,IAAI,IAAI;KACzD,OAAO;IACT;IACA,MAAM,QAAQ,SAAS,GAAG,KAAK;IAC/B,IAAI,KAAK;IACT,IAAI,KAAK,MAAM;IACf,OAAO,KAAK,IAAI;KACd,MAAM,MAAO,KAAK,MAAO;KACzB,IAAI,MAAM,GAAG,OAAQ,GAAG,KAAK,MAAM;UAC9B,KAAK;IACZ;IAEA,OAAO,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI;IAClC,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,MAAM,GAAG,KAAK,KAAM,CAAC;IACvD,IAAI,OAAO;IACX,IAAI,WAAW;IACf,KAAK,IAAI,UAAU,GAAG,UAAU,QAAQ;KACtC,UAAU,KAAK,IAAI,OAAO,KAAK,OAAO,CAAC;KACvC,MAAM,OAAO,OAAO,OAAO;KAC3B,IAAI,QAAQ,KAAK,IAAI,MAAM,QAAQ,IAAI,IAAI,SAAS,IAAI,GAAG;MACzD,OAAO;MACP,WAAW;KACb;IACF;IACA,IAAI;IACJ,IAAI;GACN;GACA,OAAO,KAAK,GAAG,CAAC;EAClB;EAGA,IAAI,KAAK,IAAI,MAAM,KAAK,CAAC,CAAC;CAC5B;AACF"}
|
package/dist/lines.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Cut, type CutOptions, type Protect } from "./cut.js";
|
|
2
|
+
/** Text prepared for laying out in lines with pretext. */
|
|
3
|
+
export interface PreparedLines {
|
|
4
|
+
/** The text as its white space mode paints it; every offset refers to this string. */
|
|
5
|
+
readonly text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PrepareLinesOptions {
|
|
8
|
+
/** CSS `white-space`. Default "normal". */
|
|
9
|
+
readonly whiteSpace?: "normal" | "pre-wrap";
|
|
10
|
+
readonly wordBreak?: "normal" | "keep-all";
|
|
11
|
+
/** CSS `letter-spacing` in px. */
|
|
12
|
+
readonly letterSpacing?: number;
|
|
13
|
+
/**
|
|
14
|
+
* As in `prepare`, but a clamp counts only whole matches, on its last line, and keeps
|
|
15
|
+
* each whole or drops it whole.
|
|
16
|
+
*/
|
|
17
|
+
readonly protect?: Protect;
|
|
18
|
+
}
|
|
19
|
+
export interface ClampOptions extends CutOptions {
|
|
20
|
+
/** How many lines to keep. */
|
|
21
|
+
readonly lines: number;
|
|
22
|
+
/** Room in px to keep free after the text's end, for an element such as a "more" link. */
|
|
23
|
+
readonly after?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const prepareLines: (text: string, font: string, options?: PrepareLinesOptions) => PreparedLines;
|
|
26
|
+
/** The room in px after the last line of the whole text, where a "less" link would go. */
|
|
27
|
+
export declare const roomAfter: (lines: PreparedLines, width: number) => number;
|
|
28
|
+
/**
|
|
29
|
+
* Cuts text to `options.lines` lines of `width` px, with the ellipsis at the end of the
|
|
30
|
+
* last line, the start of the first, or inside a middle line (`ratio` of the way through).
|
|
31
|
+
*/
|
|
32
|
+
export declare const clamp: (lines: PreparedLines, width: number, options: ClampOptions) => Cut;
|
package/dist/lines.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as clamp, n as prepareLines, r as roomAfter } from "./lines-DOj2DQLl.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as fontkit from "fontkit";
|
|
2
|
+
//#region scripts/metrics.ts
|
|
3
|
+
/** The metrics of the font at `file`, for the characters it maps (or only those in `only`). */
|
|
4
|
+
var readMetrics = (file, family, only) => {
|
|
5
|
+
const font = fontkit.openSync(file);
|
|
6
|
+
const width = (text) => {
|
|
7
|
+
let sum = 0;
|
|
8
|
+
for (const position of font.layout(text).positions) sum += position.xAdvance;
|
|
9
|
+
return sum;
|
|
10
|
+
};
|
|
11
|
+
const codes = [];
|
|
12
|
+
for (const code of only ? Array.from(only, (c) => c.codePointAt(0)) : font.characterSet) if (code >= 32 && font.hasGlyphForCodePoint(code)) codes.push(code);
|
|
13
|
+
codes.sort((a, b) => a - b);
|
|
14
|
+
const chars = String.fromCodePoint(...codes);
|
|
15
|
+
const glyphs = Array.from(chars);
|
|
16
|
+
const alone = Array.from(glyphs, width);
|
|
17
|
+
const kerns = /* @__PURE__ */ new Map();
|
|
18
|
+
const kerning = [];
|
|
19
|
+
for (let i = 0; i < glyphs.length; i++) for (let j = 0; j < glyphs.length; j++) {
|
|
20
|
+
const kern = width(glyphs[i] + glyphs[j]) - alone[i] - alone[j];
|
|
21
|
+
if (kern) {
|
|
22
|
+
kerning.push(i, j, kern);
|
|
23
|
+
kerns.set(glyphs[i] + glyphs[j], kern);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const neighbours = [];
|
|
27
|
+
for (const c of "0 9A Za z(-:.,") if (glyphs.includes(c)) neighbours.push(c);
|
|
28
|
+
const own = (c) => alone[glyphs.indexOf(c)];
|
|
29
|
+
const pairs = (l, c, r) => own(l) + own(c) + own(r) + (kerns.get(l + c) ?? 0) + (kerns.get(c + r) ?? 0);
|
|
30
|
+
const advances = Array.from(glyphs, (glyph, i) => {
|
|
31
|
+
let widest = alone[i];
|
|
32
|
+
for (const before of neighbours) for (const after of neighbours) {
|
|
33
|
+
const extra = width(before + glyph + after) - pairs(before, glyph, after);
|
|
34
|
+
widest = Math.max(widest, alone[i] + extra);
|
|
35
|
+
}
|
|
36
|
+
return widest;
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
family: family ?? font.familyName,
|
|
40
|
+
unitsPerEm: font.unitsPerEm,
|
|
41
|
+
chars,
|
|
42
|
+
advances,
|
|
43
|
+
kerning
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { readMetrics };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=metrics-C2jv4JVL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics-C2jv4JVL.js","names":[],"sources":["../scripts/metrics.ts"],"sourcesContent":["// Reads a font file into the widths pretrunc measures with where there is no canvas:\n// every character the font maps, and every pair of them the font kerns, laid out with\n// fontkit, which applies the font's GPOS kerning. A variable font is read at its default\n// instance. Plain loops, not effect, since pretrunc carries no dependencies. The\n// `pretrunc-metrics` command (metrics-cli.ts) writes it to a module.\nimport * as fontkit from \"fontkit\";\nimport type { Metrics } from \"../src/metrics.ts\";\n\n/** The metrics of the font at `file`, for the characters it maps (or only those in `only`). */\nexport const readMetrics = (file: string, family?: string, only?: string): Metrics => {\n const font = fontkit.openSync(file) as fontkit.Font;\n const width = (text: string) => {\n let sum = 0;\n for (const position of font.layout(text).positions) sum += position.xAdvance;\n return sum;\n };\n const codes: number[] = [];\n for (const code of only ? Array.from(only, (c) => c.codePointAt(0)!) : font.characterSet) {\n if (code >= 32 && font.hasGlyphForCodePoint(code)) codes.push(code);\n }\n codes.sort((a, b) => a - b);\n const chars = String.fromCodePoint(...codes);\n const glyphs = Array.from(chars);\n const alone = Array.from(glyphs, width);\n const kerns = new Map<string, number>();\n const kerning: number[] = [];\n for (let i = 0; i < glyphs.length; i++) {\n for (let j = 0; j < glyphs.length; j++) {\n // Against each glyph alone, so kerning never takes back a widened advance.\n const kern = width(glyphs[i]! + glyphs[j]!) - alone[i]! - alone[j]!;\n if (kern) {\n kerning.push(i, j, kern);\n kerns.set(glyphs[i]! + glyphs[j]!, kern);\n }\n }\n }\n // Contextual rules reach past a pair, as Inter spaces a colon out between digits, and\n // can make text wider than its pairs say. So each character is laid out between each\n // two of these neighbours, and whatever the three take beyond their pairs goes on its\n // advance: the table is then never narrower than an engine lays the text out, which is\n // what a cut made with it needs.\n const neighbours: string[] = [];\n for (const c of \"0 9A Za z(-:.,\") if (glyphs.includes(c)) neighbours.push(c);\n const own = (c: string) => alone[glyphs.indexOf(c)]!;\n const pairs = (l: string, c: string, r: string) =>\n own(l) + own(c) + own(r) + (kerns.get(l + c) ?? 0) + (kerns.get(c + r) ?? 0);\n const advances = Array.from(glyphs, (glyph, i) => {\n let widest = alone[i]!;\n for (const before of neighbours) {\n for (const after of neighbours) {\n const extra = width(before + glyph + after) - pairs(before, glyph, after);\n widest = Math.max(widest, alone[i]! + extra);\n }\n }\n return widest;\n });\n return {\n family: family ?? font.familyName,\n unitsPerEm: font.unitsPerEm,\n chars,\n advances,\n kerning,\n };\n};\n"],"mappings":";;;AASA,IAAa,eAAe,MAAc,QAAiB,SAA2B;CACpF,MAAM,OAAO,QAAQ,SAAS,IAAI;CAClC,MAAM,SAAS,SAAiB;EAC9B,IAAI,MAAM;EACV,KAAK,MAAM,YAAY,KAAK,OAAO,IAAI,CAAC,CAAC,WAAW,OAAO,SAAS;EACpE,OAAO;CACT;CACA,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,QAAQ,OAAO,MAAM,KAAK,OAAO,MAAM,EAAE,YAAY,CAAC,CAAE,IAAI,KAAK,cAC1E,IAAI,QAAQ,MAAM,KAAK,qBAAqB,IAAI,GAAG,MAAM,KAAK,IAAI;CAEpE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC;CAC1B,MAAM,QAAQ,OAAO,cAAc,GAAG,KAAK;CAC3C,MAAM,SAAS,MAAM,KAAK,KAAK;CAC/B,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK;CACtC,MAAM,wBAAQ,IAAI,IAAoB;CACtC,MAAM,UAAoB,CAAC;CAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EAEtC,MAAM,OAAO,MAAM,OAAO,KAAM,OAAO,EAAG,IAAI,MAAM,KAAM,MAAM;EAChE,IAAI,MAAM;GACR,QAAQ,KAAK,GAAG,GAAG,IAAI;GACvB,MAAM,IAAI,OAAO,KAAM,OAAO,IAAK,IAAI;EACzC;CACF;CAOF,MAAM,aAAuB,CAAC;CAC9B,KAAK,MAAM,KAAK,kBAAkB,IAAI,OAAO,SAAS,CAAC,GAAG,WAAW,KAAK,CAAC;CAC3E,MAAM,OAAO,MAAc,MAAM,OAAO,QAAQ,CAAC;CACjD,MAAM,SAAS,GAAW,GAAW,MACnC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,KAAK;CAC5E,MAAM,WAAW,MAAM,KAAK,SAAS,OAAO,MAAM;EAChD,IAAI,SAAS,MAAM;EACnB,KAAK,MAAM,UAAU,YACnB,KAAK,MAAM,SAAS,YAAY;GAC9B,MAAM,QAAQ,MAAM,SAAS,QAAQ,KAAK,IAAI,MAAM,QAAQ,OAAO,KAAK;GACxE,SAAS,KAAK,IAAI,QAAQ,MAAM,KAAM,KAAK;EAC7C;EAEF,OAAO;CACT,CAAC;CACD,OAAO;EACL,QAAQ,UAAU,KAAK;EACvB,YAAY,KAAK;EACjB;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
//#region scripts/metrics-cli.ts
|
|
4
|
+
var [file, out, family] = process.argv.slice(2);
|
|
5
|
+
var loaded = file && out ? await import("./metrics-C2jv4JVL.js").catch(() => void 0) : void 0;
|
|
6
|
+
if (!file || !out) {
|
|
7
|
+
process.stderr.write("Usage: pretrunc-metrics <font file> <out.ts> [family]\n");
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
} else if (!loaded) {
|
|
10
|
+
process.stderr.write("pretrunc-metrics reads fonts with fontkit: install it, as `pnpm add -D fontkit`.\n");
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
} else {
|
|
13
|
+
const metrics = loaded.readMetrics(file, family);
|
|
14
|
+
writeFileSync(out, `// Written by pretrunc-metrics from ${file.split("/").pop()}; don't edit.\nimport type { Metrics } from "pretrunc/metrics";\n\nexport default ${JSON.stringify(metrics)} satisfies Metrics;\n`);
|
|
15
|
+
process.stdout.write(`${metrics.family}: ${Array.from(metrics.chars).length} characters, ${metrics.kerning.length / 3} kerned pairs\n`);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=metrics-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics-cli.js","names":[],"sources":["../scripts/metrics-cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// `pretrunc-metrics <font file> <out.ts> [family]`: writes a font's metrics table as a\n// module, for `tableFace` from pretrunc/metrics. It ships built in dist/ as the package's\n// command, and fontkit, which reads the font, is an optional peer: only this needs it.\n// oxlint-disable-next-line effecttsgo/node-builtin-import -- a CLI writing one file; no Effect platform package for that\nimport { writeFileSync } from \"node:fs\";\n\nconst [file, out, family] = process.argv.slice(2);\n// metrics.ts imports fontkit, which a project installs only to make tables.\nconst loaded = file && out ? await import(\"./metrics.ts\").catch(() => undefined) : undefined;\nif (!file || !out) {\n process.stderr.write(\"Usage: pretrunc-metrics <font file> <out.ts> [family]\\n\");\n process.exitCode = 1;\n} else if (!loaded) {\n process.stderr.write(\n \"pretrunc-metrics reads fonts with fontkit: install it, as `pnpm add -D fontkit`.\\n\",\n );\n process.exitCode = 1;\n} else {\n const metrics = loaded.readMetrics(file, family);\n writeFileSync(\n out,\n `// Written by pretrunc-metrics from ${file.split(\"/\").pop()}; don't edit.\\n` +\n `import type { Metrics } from \"pretrunc/metrics\";\\n\\n` +\n `export default ${JSON.stringify(metrics)} satisfies Metrics;\\n`,\n );\n process.stdout.write(\n `${metrics.family}: ${Array.from(metrics.chars).length} characters, ${metrics.kerning.length / 3} kerned pairs\\n`,\n );\n}\n"],"mappings":";;;AAOA,IAAM,CAAC,MAAM,KAAK,UAAU,QAAQ,KAAK,MAAM,CAAC;AAEhD,IAAM,SAAS,QAAQ,MAAM,MAAM,OAAO,wBAAe,CAAC,YAAY,KAAA,CAAS,IAAI,KAAA;AACnF,IAAI,CAAC,QAAQ,CAAC,KAAK;CACjB,QAAQ,OAAO,MAAM,yDAAyD;CAC9E,QAAQ,WAAW;AACrB,OAAO,IAAI,CAAC,QAAQ;CAClB,QAAQ,OAAO,MACb,oFACF;CACA,QAAQ,WAAW;AACrB,OAAO;CACL,MAAM,UAAU,OAAO,YAAY,MAAM,MAAM;CAC/C,cACE,KACA,uCAAuC,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,oFAEzC,KAAK,UAAU,OAAO,EAAE,sBAC9C;CACA,QAAQ,OAAO,MACb,GAAG,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,KAAK,CAAC,CAAC,OAAO,eAAe,QAAQ,QAAQ,SAAS,EAAE,gBACnG;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Face } from "./face.js";
|
|
2
|
+
/**
|
|
3
|
+
* A font's widths, written by `scripts/metrics.ts` from the font file: what a face needs
|
|
4
|
+
* to measure text with no canvas, on a server, in a worker, or before the font loads.
|
|
5
|
+
*/
|
|
6
|
+
export interface Metrics {
|
|
7
|
+
/** The family it was read from, as CSS names it. */
|
|
8
|
+
readonly family: string;
|
|
9
|
+
readonly unitsPerEm: number;
|
|
10
|
+
/** The characters covered, in order. */
|
|
11
|
+
readonly chars: string;
|
|
12
|
+
/** Each covered character's advance, in font units, in the order of `chars`. */
|
|
13
|
+
readonly advances: ReadonlyArray<number>;
|
|
14
|
+
/** Kerned pairs as triples: first's index in `chars`, second's, adjustment in font units. */
|
|
15
|
+
readonly kerning: ReadonlyArray<number>;
|
|
16
|
+
}
|
|
17
|
+
/** Measures text in `metrics` at `px` px: each character's advance plus each pair's kerning. */
|
|
18
|
+
export declare const measure: (metrics: Metrics, px: number, text: string) => number;
|
|
19
|
+
/** A face that measures with `metrics` at `px` px, for `prepare` with no canvas. */
|
|
20
|
+
export declare const tableFace: (metrics: Metrics, px: number) => Face;
|
|
21
|
+
/**
|
|
22
|
+
* A stand-in for `OffscreenCanvas` that measures with tables instead of a font engine,
|
|
23
|
+
* for pretext and `pretrunc/lines` where there is no canvas: install it as
|
|
24
|
+
* `globalThis.OffscreenCanvas` before the first `prepareLines`. The context reads the
|
|
25
|
+
* size and the first family with a table from its `font`, and falls back to the first.
|
|
26
|
+
*/
|
|
27
|
+
export declare const tableCanvas: (tables: ReadonlyArray<Metrics>) => {
|
|
28
|
+
new (): {
|
|
29
|
+
getContext(): {
|
|
30
|
+
font: string;
|
|
31
|
+
measureText: (text: string) => {
|
|
32
|
+
width: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|