@mark1russell7/typeset 0.1.0 → 0.1.2
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 +38 -0
- package/build/breaking.d.ts +2 -1
- package/build/chunks/breaking.d.ts +2 -77
- package/build/chunks/breaking.d.ts.map +1 -1
- package/build/chunks/engines.d.ts +32 -0
- package/build/chunks/engines.d.ts.map +1 -0
- package/build/chunks/engines.js +35 -0
- package/build/chunks/engines.js.map +1 -0
- package/build/chunks/font.d.ts +203 -0
- package/build/chunks/font.d.ts.map +1 -0
- package/build/chunks/font.js +3550 -0
- package/build/chunks/font.js.map +1 -0
- package/build/chunks/fonts.d.ts +8 -0
- package/build/chunks/fonts.d.ts.map +1 -0
- package/build/chunks/fonts.js +13 -0
- package/build/chunks/fonts.js.map +1 -0
- package/build/chunks/measurer.d.ts +67 -0
- package/build/chunks/measurer.d.ts.map +1 -0
- package/build/chunks/measurer.js +277 -0
- package/build/chunks/measurer.js.map +1 -0
- package/build/chunks/paragraph.d.ts +78 -0
- package/build/chunks/paragraph.d.ts.map +1 -0
- package/build/chunks/sfnt.d.ts +22 -0
- package/build/chunks/sfnt.d.ts.map +1 -0
- package/build/chunks/text.d.ts +48 -0
- package/build/chunks/text.d.ts.map +1 -0
- package/build/chunks/text.js +237 -0
- package/build/chunks/text.js.map +1 -0
- package/build/fonts-inspect.d.ts +10 -0
- package/build/fonts-inspect.d.ts.map +1 -0
- package/build/fonts-inspect.js +1 -0
- package/build/fonts.d.ts +5 -0
- package/build/fonts.js +4 -0
- package/build/index.d.ts +4 -1
- package/build/index.js +5 -1
- package/build/measurer.d.ts +2 -0
- package/build/measurer.js +1 -0
- package/build/text.d.ts +2 -0
- package/build/text.js +1 -0
- package/package.json +22 -3
- package/src/fonts/bidi.ts +125 -0
- package/src/fonts/binary.ts +41 -0
- package/src/fonts/engines.ts +38 -0
- package/src/fonts/font.ts +442 -0
- package/src/fonts/gvar.ts +220 -0
- package/src/fonts/index.ts +10 -0
- package/src/fonts/indic.ts +585 -0
- package/src/fonts/joining.ts +141 -0
- package/src/fonts/kerning.ts +217 -0
- package/src/fonts/sfnt.ts +102 -0
- package/src/fonts/substitution.ts +586 -0
- package/src/fonts/tables.ts +140 -0
- package/src/fonts/variations.ts +175 -0
- package/src/fonts/words.ts +119 -0
- package/src/fonts-inspect.ts +8 -0
- package/src/fonts.ts +8 -0
- package/src/index.ts +3 -0
- package/src/measurer/advance.ts +307 -0
- package/src/measurer.ts +5 -0
- package/src/text/rope.ts +291 -0
- package/src/text.ts +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
+
// src/text/rope.ts
|
|
3
|
+
function isSeparator(context, code) {
|
|
4
|
+
for (let i = 0; i < context.separators.length; i++) if (context.separators.charCodeAt(i) === code) return true;
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
function leafOf(context, text) {
|
|
8
|
+
const width = context.width(text);
|
|
9
|
+
let first = -1;
|
|
10
|
+
let last = -1;
|
|
11
|
+
for (let i = 0; i < text.length; i++) {
|
|
12
|
+
if (!isSeparator(context, text.charCodeAt(i))) continue;
|
|
13
|
+
if (first < 0) first = i;
|
|
14
|
+
last = i;
|
|
15
|
+
}
|
|
16
|
+
if (first < 0) return {
|
|
17
|
+
kind: "leaf",
|
|
18
|
+
text,
|
|
19
|
+
height: 0,
|
|
20
|
+
length: text.length,
|
|
21
|
+
width,
|
|
22
|
+
separated: false,
|
|
23
|
+
head: text,
|
|
24
|
+
headWidth: width,
|
|
25
|
+
tail: text,
|
|
26
|
+
tailWidth: width
|
|
27
|
+
};
|
|
28
|
+
const head = text.slice(0, first + 1);
|
|
29
|
+
const tail = text.slice(last);
|
|
30
|
+
return {
|
|
31
|
+
kind: "leaf",
|
|
32
|
+
text,
|
|
33
|
+
height: 0,
|
|
34
|
+
length: text.length,
|
|
35
|
+
width,
|
|
36
|
+
separated: true,
|
|
37
|
+
head,
|
|
38
|
+
headWidth: context.width(head),
|
|
39
|
+
tail,
|
|
40
|
+
tailWidth: context.width(tail)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* This function makes the branch of two subtrees. It measures one window: the tail of the left side and the head of the right
|
|
45
|
+
* side. The separator law then gives the width of all the text from the widths of the two sides.
|
|
46
|
+
*/
|
|
47
|
+
function branchOf(context, left, right) {
|
|
48
|
+
const seam = left.tail + right.head;
|
|
49
|
+
const seamWidth = context.width(seam);
|
|
50
|
+
let width;
|
|
51
|
+
if (!left.separated && !right.separated) width = seamWidth;
|
|
52
|
+
else if (!left.separated) width = right.width - right.headWidth + seamWidth;
|
|
53
|
+
else if (!right.separated) width = left.width - left.tailWidth + seamWidth;
|
|
54
|
+
else width = left.width + right.width - left.tailWidth - right.headWidth + seamWidth;
|
|
55
|
+
return {
|
|
56
|
+
kind: "branch",
|
|
57
|
+
left,
|
|
58
|
+
right,
|
|
59
|
+
height: Math.max(left.height, right.height) + 1,
|
|
60
|
+
length: left.length + right.length,
|
|
61
|
+
width,
|
|
62
|
+
separated: left.separated || right.separated,
|
|
63
|
+
head: left.separated ? left.head : seam,
|
|
64
|
+
headWidth: left.separated ? left.headWidth : seamWidth,
|
|
65
|
+
tail: right.separated ? right.tail : seam,
|
|
66
|
+
tailWidth: right.separated ? right.tailWidth : seamWidth
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** This function joins two trees in the order of the text and keeps the heights of the two children within 1. */
|
|
70
|
+
function join(context, left, right) {
|
|
71
|
+
if (left === void 0) return right;
|
|
72
|
+
if (right === void 0) return left;
|
|
73
|
+
if (left.kind === "leaf" && right.kind === "leaf" && left.length + right.length <= context.chunk) return leafOf(context, left.text + right.text);
|
|
74
|
+
if (left.height > right.height + 1) {
|
|
75
|
+
const branch = left;
|
|
76
|
+
const joined = join(context, branch.right, right);
|
|
77
|
+
if (joined.height <= branch.left.height + 1) return branchOf(context, branch.left, joined);
|
|
78
|
+
return rotateLeft(context, branch.left, joined);
|
|
79
|
+
}
|
|
80
|
+
if (right.height > left.height + 1) {
|
|
81
|
+
const branch = right;
|
|
82
|
+
const joined = join(context, left, branch.left);
|
|
83
|
+
if (joined.height <= branch.right.height + 1) return branchOf(context, joined, branch.right);
|
|
84
|
+
return rotateRight(context, joined, branch.right);
|
|
85
|
+
}
|
|
86
|
+
return branchOf(context, left, right);
|
|
87
|
+
}
|
|
88
|
+
/** This function balances the branch of `left` and `heavy`, when `heavy` is 2 levels higher than `left`. */
|
|
89
|
+
function rotateLeft(context, left, heavy) {
|
|
90
|
+
if (heavy.left.height <= heavy.right.height) return branchOf(context, branchOf(context, left, heavy.left), heavy.right);
|
|
91
|
+
const middle = heavy.left;
|
|
92
|
+
return branchOf(context, branchOf(context, left, middle.left), branchOf(context, middle.right, heavy.right));
|
|
93
|
+
}
|
|
94
|
+
/** This function balances the branch of `heavy` and `right`, when `heavy` is 2 levels higher than `right`. */
|
|
95
|
+
function rotateRight(context, heavy, right) {
|
|
96
|
+
if (heavy.right.height <= heavy.left.height) return branchOf(context, heavy.left, branchOf(context, heavy.right, right));
|
|
97
|
+
const middle = heavy.right;
|
|
98
|
+
return branchOf(context, branchOf(context, heavy.left, middle.left), branchOf(context, middle.right, right));
|
|
99
|
+
}
|
|
100
|
+
/** This function divides a tree at the UTF-16 offset `at` into the text before and the text after. */
|
|
101
|
+
function split(context, node, at) {
|
|
102
|
+
if (node === void 0) return [void 0, void 0];
|
|
103
|
+
if (at <= 0) return [void 0, node];
|
|
104
|
+
if (at >= node.length) return [node, void 0];
|
|
105
|
+
if (node.kind === "leaf") return [leafOf(context, node.text.slice(0, at)), leafOf(context, node.text.slice(at))];
|
|
106
|
+
if (at === node.left.length) return [node.left, node.right];
|
|
107
|
+
if (at < node.left.length) {
|
|
108
|
+
const [before, after] = split(context, node.left, at);
|
|
109
|
+
return [before, join(context, after, node.right)];
|
|
110
|
+
}
|
|
111
|
+
const [before, after] = split(context, node.right, at - node.left.length);
|
|
112
|
+
return [join(context, node.left, before), after];
|
|
113
|
+
}
|
|
114
|
+
/** This function gives the end of the next leaf of `text` from `start`: after a separator when it can, and not in a surrogate pair. */
|
|
115
|
+
function chunkEnd(context, text, start) {
|
|
116
|
+
const limit = start + context.chunk;
|
|
117
|
+
if (limit >= text.length) return text.length;
|
|
118
|
+
for (let i = limit - 1; i >= start + context.chunk / 2; i--) if (isSeparator(context, text.charCodeAt(i))) return i + 1;
|
|
119
|
+
const code = text.charCodeAt(limit - 1);
|
|
120
|
+
return code >= 55296 && code <= 56319 ? limit - 1 : limit;
|
|
121
|
+
}
|
|
122
|
+
/** This function makes a balanced tree of a text: leaves of at most `chunk` code units, and then a level of branches at a time. */
|
|
123
|
+
function build(context, text) {
|
|
124
|
+
let level = [];
|
|
125
|
+
for (let start = 0; start < text.length;) {
|
|
126
|
+
const end = chunkEnd(context, text, start);
|
|
127
|
+
level.push(leafOf(context, text.slice(start, end)));
|
|
128
|
+
start = end;
|
|
129
|
+
}
|
|
130
|
+
if (level.length === 0) return void 0;
|
|
131
|
+
while (level.length > 1) {
|
|
132
|
+
const next = [];
|
|
133
|
+
for (let i = 0; i + 1 < level.length; i += 2) next.push(branchOf(context, level[i], level[i + 1]));
|
|
134
|
+
if (level.length % 2 === 1) next.push(level[level.length - 1]);
|
|
135
|
+
level = next;
|
|
136
|
+
}
|
|
137
|
+
return level[0];
|
|
138
|
+
}
|
|
139
|
+
function leaves(node, out) {
|
|
140
|
+
if (node === void 0) return out;
|
|
141
|
+
const stack = [node];
|
|
142
|
+
while (stack.length > 0) {
|
|
143
|
+
const top = stack.pop();
|
|
144
|
+
if (top.kind === "leaf") out.push(top.text);
|
|
145
|
+
else stack.push(top.right, top.left);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A text with its width: a persistent balanced rope. Each edit gives a new `Text` and does not change this one. The offsets
|
|
151
|
+
* are in UTF-16 code units, as the offsets of a JavaScript string.
|
|
152
|
+
*/
|
|
153
|
+
var Text = class Text {
|
|
154
|
+
#context;
|
|
155
|
+
#root;
|
|
156
|
+
constructor(context, root) {
|
|
157
|
+
this.#context = context;
|
|
158
|
+
this.#root = root;
|
|
159
|
+
}
|
|
160
|
+
/** This function makes the rope of `text` with the width function `width`. */
|
|
161
|
+
static from(text, width, options = {}) {
|
|
162
|
+
const separators = options.separators ?? " ";
|
|
163
|
+
const chunk = options.chunk ?? 256;
|
|
164
|
+
if (!Number.isInteger(chunk) || chunk < 2) throw new RangeError(`the chunk is ${chunk}, not an integer of 2 or more`);
|
|
165
|
+
for (let i = 0; i < separators.length; i++) {
|
|
166
|
+
const code = separators.charCodeAt(i);
|
|
167
|
+
if (code >= 55296 && code <= 57343) throw new RangeError("a separator is one UTF-16 code unit, not a half of a surrogate pair");
|
|
168
|
+
}
|
|
169
|
+
const context = {
|
|
170
|
+
width,
|
|
171
|
+
separators,
|
|
172
|
+
chunk
|
|
173
|
+
};
|
|
174
|
+
return new Text(context, build(context, text));
|
|
175
|
+
}
|
|
176
|
+
/** The count of UTF-16 code units of the text. */
|
|
177
|
+
get length() {
|
|
178
|
+
return this.#root?.length ?? 0;
|
|
179
|
+
}
|
|
180
|
+
/** The width of the text. It is equal to the width of the flat string when the width function obeys the separator law. */
|
|
181
|
+
get width() {
|
|
182
|
+
return this.#root?.width ?? 0;
|
|
183
|
+
}
|
|
184
|
+
/** The height of the tree: 0 for a text of one leaf. The tests use it to examine the balance. */
|
|
185
|
+
get height() {
|
|
186
|
+
return this.#root?.height ?? 0;
|
|
187
|
+
}
|
|
188
|
+
/** This function gives the flat string. */
|
|
189
|
+
toString() {
|
|
190
|
+
return leaves(this.#root, []).join("");
|
|
191
|
+
}
|
|
192
|
+
/** This function gives the strings of the leaves in the order of the text. */
|
|
193
|
+
chunks() {
|
|
194
|
+
return leaves(this.#root, []);
|
|
195
|
+
}
|
|
196
|
+
/** This function gives the text from `start` to `end`, as a new rope. */
|
|
197
|
+
slice(start, end = this.length) {
|
|
198
|
+
this.#check(start, end);
|
|
199
|
+
const [, after] = split(this.#context, this.#root, start);
|
|
200
|
+
const [middle] = split(this.#context, after, end - start);
|
|
201
|
+
return new Text(this.#context, middle);
|
|
202
|
+
}
|
|
203
|
+
/** This function gives the width of the text from `start` to `end`. */
|
|
204
|
+
widthOf(start, end = this.length) {
|
|
205
|
+
return this.slice(start, end).width;
|
|
206
|
+
}
|
|
207
|
+
/** This function gives this text and then `other`. The two ropes must have the same width function and options. */
|
|
208
|
+
concat(other) {
|
|
209
|
+
const context = other.#context;
|
|
210
|
+
if (context.width !== this.#context.width || context.separators !== this.#context.separators || context.chunk !== this.#context.chunk) throw new TypeError("the two ropes have different width functions or options");
|
|
211
|
+
return new Text(this.#context, join(this.#context, this.#root, other.#root));
|
|
212
|
+
}
|
|
213
|
+
/** This function puts `text` at the offset `at`. */
|
|
214
|
+
insert(at, text) {
|
|
215
|
+
return this.replace(at, at, text);
|
|
216
|
+
}
|
|
217
|
+
/** This function removes the text from `start` to `end`. */
|
|
218
|
+
remove(start, end) {
|
|
219
|
+
return this.replace(start, end, "");
|
|
220
|
+
}
|
|
221
|
+
/** This function puts `text` in place of the text from `start` to `end`. */
|
|
222
|
+
replace(start, end, text) {
|
|
223
|
+
this.#check(start, end);
|
|
224
|
+
const [before, rest] = split(this.#context, this.#root, start);
|
|
225
|
+
const [, after] = split(this.#context, rest, end - start);
|
|
226
|
+
const middle = build(this.#context, text);
|
|
227
|
+
return new Text(this.#context, join(this.#context, join(this.#context, before, middle), after));
|
|
228
|
+
}
|
|
229
|
+
#check(start, end) {
|
|
230
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > this.length) throw new RangeError(`the range ${start} to ${end} is not in the text of length ${this.length}`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
// src/text.ts
|
|
234
|
+
var text_exports = /* @__PURE__ */ __exportAll({ Text: () => Text });
|
|
235
|
+
export { Text as n, text_exports as t };
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../src/text/rope.ts","../../src/text.ts"],"sourcesContent":["/**\n * The `Text` rope: a persistent balanced tree of strings with the width and the length of each subtree. An edit makes a\n * new rope. The new rope shares the subtrees that do not change. A width function gives the width of a run of text. The\n * rope uses it only for the leaves and for the windows at the seams. Thus an edit uses it of the order of `log n` times.\n *\n * The seams are exact when the width function obeys the separator law. For each text `p + s + q` with a separator `s`:\n * `width(p + s + q) = width(p + s) + width(s + q) - width(s)`. The model of `@mark1russell7/typeset/fonts` obeys it for a space and a\n * tab. It shapes each word alone, and its kerning goes only between two adjacent glyphs.\n */\nimport type { WidthOf } from \"../breaking/paragraph.ts\";\n\n/** The options of a rope. */\nexport interface TextOptions {\n /**\n * The separators of the separator law: each character is one UTF-16 code unit. The default is a space and a tab. An\n * empty string makes each seam measure the full text of the two sides.\n */\n readonly separators?: string;\n /** The largest count of UTF-16 code units in a leaf. The default is 256. */\n readonly chunk?: number;\n}\n\n/** The summary of a subtree: its length, its width, and the two ends that a seam measures again. */\ninterface Summary {\n /** The count of UTF-16 code units. */\n readonly length: number;\n readonly width: number;\n /** This value is `true` when the text has a separator. */\n readonly separated: boolean;\n /** The text through the first separator, or all the text when the text has no separator. */\n readonly head: string;\n readonly headWidth: number;\n /** The text from the last separator, or all the text when the text has no separator. */\n readonly tail: string;\n readonly tailWidth: number;\n}\n\ninterface Leaf extends Summary {\n readonly kind: \"leaf\";\n readonly text: string;\n readonly height: 0;\n}\n\ninterface Branch extends Summary {\n readonly kind: \"branch\";\n readonly left: Node;\n readonly right: Node;\n readonly height: number;\n}\n\ntype Node = Leaf | Branch;\n\n/** The shared state of the ropes of one width function and one set of options. */\ninterface Context {\n readonly width: WidthOf;\n readonly separators: string;\n readonly chunk: number;\n}\n\nfunction isSeparator(context: Context, code: number): boolean {\n for (let i = 0; i < context.separators.length; i++) if (context.separators.charCodeAt(i) === code) return true;\n return false;\n}\n\nfunction leafOf(context: Context, text: string): Leaf {\n const width = context.width(text);\n let first = -1;\n let last = -1;\n for (let i = 0; i < text.length; i++) {\n if (!isSeparator(context, text.charCodeAt(i))) continue;\n if (first < 0) first = i;\n last = i;\n }\n if (first < 0) return { kind: \"leaf\", text, height: 0, length: text.length, width, separated: false, head: text, headWidth: width, tail: text, tailWidth: width };\n const head = text.slice(0, first + 1);\n const tail = text.slice(last);\n return { kind: \"leaf\", text, height: 0, length: text.length, width, separated: true, head, headWidth: context.width(head), tail, tailWidth: context.width(tail) };\n}\n\n/**\n * This function makes the branch of two subtrees. It measures one window: the tail of the left side and the head of the right\n * side. The separator law then gives the width of all the text from the widths of the two sides.\n */\nfunction branchOf(context: Context, left: Node, right: Node): Branch {\n const seam = left.tail + right.head;\n const seamWidth = context.width(seam);\n let width: number;\n if (!left.separated && !right.separated) width = seamWidth;\n else if (!left.separated) width = right.width - right.headWidth + seamWidth;\n else if (!right.separated) width = left.width - left.tailWidth + seamWidth;\n else width = left.width + right.width - left.tailWidth - right.headWidth + seamWidth;\n return {\n kind: \"branch\",\n left,\n right,\n height: Math.max(left.height, right.height) + 1,\n length: left.length + right.length,\n width,\n separated: left.separated || right.separated,\n head: left.separated ? left.head : seam,\n headWidth: left.separated ? left.headWidth : seamWidth,\n tail: right.separated ? right.tail : seam,\n tailWidth: right.separated ? right.tailWidth : seamWidth,\n };\n}\n\n/** This function joins two trees in the order of the text and keeps the heights of the two children within 1. */\nfunction join(context: Context, left: Node | undefined, right: Node | undefined): Node | undefined {\n if (left === undefined) return right;\n if (right === undefined) return left;\n if (left.kind === \"leaf\" && right.kind === \"leaf\" && left.length + right.length <= context.chunk) return leafOf(context, left.text + right.text);\n if (left.height > right.height + 1) {\n const branch = left as Branch;\n const joined = join(context, branch.right, right)!;\n if (joined.height <= branch.left.height + 1) return branchOf(context, branch.left, joined);\n return rotateLeft(context, branch.left, joined as Branch);\n }\n if (right.height > left.height + 1) {\n const branch = right as Branch;\n const joined = join(context, left, branch.left)!;\n if (joined.height <= branch.right.height + 1) return branchOf(context, joined, branch.right);\n return rotateRight(context, joined as Branch, branch.right);\n }\n return branchOf(context, left, right);\n}\n\n/** This function balances the branch of `left` and `heavy`, when `heavy` is 2 levels higher than `left`. */\nfunction rotateLeft(context: Context, left: Node, heavy: Branch): Branch {\n if (heavy.left.height <= heavy.right.height) return branchOf(context, branchOf(context, left, heavy.left), heavy.right);\n const middle = heavy.left as Branch;\n return branchOf(context, branchOf(context, left, middle.left), branchOf(context, middle.right, heavy.right));\n}\n\n/** This function balances the branch of `heavy` and `right`, when `heavy` is 2 levels higher than `right`. */\nfunction rotateRight(context: Context, heavy: Branch, right: Node): Branch {\n if (heavy.right.height <= heavy.left.height) return branchOf(context, heavy.left, branchOf(context, heavy.right, right));\n const middle = heavy.right as Branch;\n return branchOf(context, branchOf(context, heavy.left, middle.left), branchOf(context, middle.right, right));\n}\n\n/** This function divides a tree at the UTF-16 offset `at` into the text before and the text after. */\nfunction split(context: Context, node: Node | undefined, at: number): [Node | undefined, Node | undefined] {\n if (node === undefined) return [undefined, undefined];\n if (at <= 0) return [undefined, node];\n if (at >= node.length) return [node, undefined];\n if (node.kind === \"leaf\") return [leafOf(context, node.text.slice(0, at)), leafOf(context, node.text.slice(at))];\n if (at === node.left.length) return [node.left, node.right];\n if (at < node.left.length) {\n const [before, after] = split(context, node.left, at);\n return [before, join(context, after, node.right)];\n }\n const [before, after] = split(context, node.right, at - node.left.length);\n return [join(context, node.left, before), after];\n}\n\n/** This function gives the end of the next leaf of `text` from `start`: after a separator when it can, and not in a surrogate pair. */\nfunction chunkEnd(context: Context, text: string, start: number): number {\n const limit = start + context.chunk;\n if (limit >= text.length) return text.length;\n for (let i = limit - 1; i >= start + context.chunk / 2; i--) if (isSeparator(context, text.charCodeAt(i))) return i + 1;\n const code = text.charCodeAt(limit - 1);\n return code >= 0xd800 && code <= 0xdbff ? limit - 1 : limit;\n}\n\n/** This function makes a balanced tree of a text: leaves of at most `chunk` code units, and then a level of branches at a time. */\nfunction build(context: Context, text: string): Node | undefined {\n let level: Node[] = [];\n for (let start = 0; start < text.length; ) {\n const end = chunkEnd(context, text, start);\n level.push(leafOf(context, text.slice(start, end)));\n start = end;\n }\n if (level.length === 0) return undefined;\n while (level.length > 1) {\n const next: Node[] = [];\n for (let i = 0; i + 1 < level.length; i += 2) next.push(branchOf(context, level[i]!, level[i + 1]!));\n if (level.length % 2 === 1) next.push(level[level.length - 1]!);\n level = next;\n }\n return level[0];\n}\n\nfunction leaves(node: Node | undefined, out: string[]): string[] {\n if (node === undefined) return out;\n const stack: Node[] = [node];\n while (stack.length > 0) {\n const top = stack.pop()!;\n if (top.kind === \"leaf\") out.push(top.text);\n else stack.push(top.right, top.left);\n }\n return out;\n}\n\n/**\n * A text with its width: a persistent balanced rope. Each edit gives a new `Text` and does not change this one. The offsets\n * are in UTF-16 code units, as the offsets of a JavaScript string.\n */\nexport class Text {\n readonly #context: Context;\n readonly #root: Node | undefined;\n\n private constructor(context: Context, root: Node | undefined) {\n this.#context = context;\n this.#root = root;\n }\n\n /** This function makes the rope of `text` with the width function `width`. */\n static from(text: string, width: WidthOf, options: TextOptions = {}): Text {\n const separators = options.separators ?? \" \\t\";\n const chunk = options.chunk ?? 256;\n if (!Number.isInteger(chunk) || chunk < 2) throw new RangeError(`the chunk is ${chunk}, not an integer of 2 or more`);\n for (let i = 0; i < separators.length; i++) {\n const code = separators.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdfff) throw new RangeError(\"a separator is one UTF-16 code unit, not a half of a surrogate pair\");\n }\n const context: Context = { width, separators, chunk };\n return new Text(context, build(context, text));\n }\n\n /** The count of UTF-16 code units of the text. */\n get length(): number {\n return this.#root?.length ?? 0;\n }\n\n /** The width of the text. It is equal to the width of the flat string when the width function obeys the separator law. */\n get width(): number {\n return this.#root?.width ?? 0;\n }\n\n /** The height of the tree: 0 for a text of one leaf. The tests use it to examine the balance. */\n get height(): number {\n return this.#root?.height ?? 0;\n }\n\n /** This function gives the flat string. */\n toString(): string {\n return leaves(this.#root, []).join(\"\");\n }\n\n /** This function gives the strings of the leaves in the order of the text. */\n chunks(): string[] {\n return leaves(this.#root, []);\n }\n\n /** This function gives the text from `start` to `end`, as a new rope. */\n slice(start: number, end: number = this.length): Text {\n this.#check(start, end);\n const [, after] = split(this.#context, this.#root, start);\n const [middle] = split(this.#context, after, end - start);\n return new Text(this.#context, middle);\n }\n\n /** This function gives the width of the text from `start` to `end`. */\n widthOf(start: number, end: number = this.length): number {\n return this.slice(start, end).width;\n }\n\n /** This function gives this text and then `other`. The two ropes must have the same width function and options. */\n concat(other: Text): Text {\n const context = other.#context;\n if (context.width !== this.#context.width || context.separators !== this.#context.separators || context.chunk !== this.#context.chunk) {\n throw new TypeError(\"the two ropes have different width functions or options\");\n }\n return new Text(this.#context, join(this.#context, this.#root, other.#root));\n }\n\n /** This function puts `text` at the offset `at`. */\n insert(at: number, text: string): Text {\n return this.replace(at, at, text);\n }\n\n /** This function removes the text from `start` to `end`. */\n remove(start: number, end: number): Text {\n return this.replace(start, end, \"\");\n }\n\n /** This function puts `text` in place of the text from `start` to `end`. */\n replace(start: number, end: number, text: string): Text {\n this.#check(start, end);\n const [before, rest] = split(this.#context, this.#root, start);\n const [, after] = split(this.#context, rest, end - start);\n const middle = build(this.#context, text);\n return new Text(this.#context, join(this.#context, join(this.#context, before, middle), after));\n }\n\n #check(start: number, end: number): void {\n if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > this.length) {\n throw new RangeError(`the range ${start} to ${end} is not in the text of length ${this.length}`);\n }\n }\n}\n","export { Text, type TextOptions } from \"./text/rope.ts\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { i as glyphAdvances, s as shapingWords, u as defaultFeatures } from "./chunks/font.js";
|
|
2
|
+
import { n as readTables } from "./chunks/sfnt.js";
|
|
3
|
+
/**
|
|
4
|
+
* This function divides the text of a canvas into the words that Blink shapes one at a time. Each space, tab and zero
|
|
5
|
+
* width space is a word alone. Each CJK character starts a word, with its marks and the CJK characters of the script
|
|
6
|
+
* Common after it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function canvasWords(text: string): string[];
|
|
9
|
+
export { defaultFeatures, glyphAdvances, readTables, shapingWords };
|
|
10
|
+
//# sourceMappingURL=fonts-inspect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fonts-inspect.d.ts","names":[],"sources":["../src/fonts/words.ts"],"mappings":";;;;;;;wBAwDgB,YAAY"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { a as canvasWords, o as defaultFeatures, t as glyphAdvances, s as readTables, i as shapingWords } from "./chunks/font.js";
|
package/build/fonts.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { t as Decompress } from "./chunks/sfnt.js";
|
|
2
|
+
import { a as measureWidth, c as Axis, l as Instance, n as MeasureOptions, o as readFont, r as ReadOptions, t as Font } from "./chunks/font.js";
|
|
3
|
+
import { i as isEngineName, n as EngineRules, r as engines, t as EngineName } from "./chunks/engines.js";
|
|
4
|
+
import "./chunks/fonts.js";
|
|
5
|
+
export { type Axis, type Decompress, type EngineName, type EngineRules, type Font, type Instance, type MeasureOptions, type ReadOptions, engines, isEngineName, measureWidth, readFont };
|
package/build/fonts.js
ADDED
package/build/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import { t as breaking_d_exports } from "./chunks/breaking.js";
|
|
2
|
-
|
|
2
|
+
import { t as fonts_d_exports } from "./chunks/fonts.js";
|
|
3
|
+
import { t as measurer_d_exports } from "./chunks/measurer.js";
|
|
4
|
+
import { t as text_d_exports } from "./chunks/text.js";
|
|
5
|
+
export { breaking_d_exports as breaking, fonts_d_exports as fonts, measurer_d_exports as measurer, text_d_exports as text };
|
package/build/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { t as breaking_exports } from "./chunks/breaking.js";
|
|
2
|
+
import { t as fonts_exports } from "./chunks/fonts.js";
|
|
3
|
+
import { t as measurer_exports } from "./chunks/measurer.js";
|
|
4
|
+
import { t as text_exports } from "./chunks/text.js";
|
|
5
|
+
export { breaking_exports as breaking, fonts_exports as fonts, measurer_exports as measurer, text_exports as text };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as fontAdvance, r as parseFont, i as scriptTag } from "./chunks/measurer.js";
|
package/build/text.d.ts
ADDED
package/build/text.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as Text } from "./chunks/text.js";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/typeset",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"description": "Text measurement and line breaking. The subpath ./
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"description": "Text measurement and line breaking. The subpath ./fonts reads font tables and gives widths, ./measurer gives the advance function of a layout, ./breaking has the line breakers, and ./text has a rope.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"typeset",
|
|
8
8
|
"line-breaking",
|
|
@@ -34,6 +34,22 @@
|
|
|
34
34
|
"types": "./build/breaking.d.ts",
|
|
35
35
|
"default": "./build/breaking.js"
|
|
36
36
|
},
|
|
37
|
+
"./fonts": {
|
|
38
|
+
"types": "./build/fonts.d.ts",
|
|
39
|
+
"default": "./build/fonts.js"
|
|
40
|
+
},
|
|
41
|
+
"./fonts/inspect": {
|
|
42
|
+
"types": "./build/fonts-inspect.d.ts",
|
|
43
|
+
"default": "./build/fonts-inspect.js"
|
|
44
|
+
},
|
|
45
|
+
"./measurer": {
|
|
46
|
+
"types": "./build/measurer.d.ts",
|
|
47
|
+
"default": "./build/measurer.js"
|
|
48
|
+
},
|
|
49
|
+
"./text": {
|
|
50
|
+
"types": "./build/text.d.ts",
|
|
51
|
+
"default": "./build/text.js"
|
|
52
|
+
},
|
|
37
53
|
"./package.json": "./package.json"
|
|
38
54
|
},
|
|
39
55
|
"files": [
|
|
@@ -48,9 +64,12 @@
|
|
|
48
64
|
"publishConfig": {
|
|
49
65
|
"access": "public"
|
|
50
66
|
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@mark1russell7/rigor-generators": "^0.1.0"
|
|
69
|
+
},
|
|
51
70
|
"devDependencies": {
|
|
52
71
|
"@mark1russell7/cue": "^1.0.2",
|
|
53
|
-
"@mark1russell7/layout-contract": "^0.2
|
|
72
|
+
"@mark1russell7/layout-contract": "^0.3.2",
|
|
54
73
|
"@types/node": "^26.6.5",
|
|
55
74
|
"@typeset/corpus": "0.1.0",
|
|
56
75
|
"tsdown": "^0.23.1",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small model of the Unicode Bidirectional Algorithm (UAX #9) for one paragraph with the base level 0. Blink adds the
|
|
3
|
+
* widths of the words of a canvas in the visual order, so the single-precision sum needs this order. The model has no
|
|
4
|
+
* explicit embeddings, no isolates and no brackets (rule N0). It gets the bidi class of a character from its script and
|
|
5
|
+
* its general category, so it knows the classes of the common characters only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A bidi class of UAX #9. */
|
|
9
|
+
type BidiClass = "L" | "R" | "AL" | "EN" | "ES" | "ET" | "AN" | "CS" | "NSM" | "BN" | "B" | "S" | "WS" | "ON";
|
|
10
|
+
|
|
11
|
+
const nonSpacingMark = /[\p{Mn}\p{Me}]/u;
|
|
12
|
+
const currency = /\p{Sc}/u;
|
|
13
|
+
const format = /\p{Cf}/u;
|
|
14
|
+
const hebrew = /\p{Script_Extensions=Hebrew}/u;
|
|
15
|
+
const arabicLetter = /[\p{Script_Extensions=Arabic}\p{Script_Extensions=Syriac}\p{Script_Extensions=Thaana}]/u;
|
|
16
|
+
const letter = /[\p{L}\p{Mc}]/u;
|
|
17
|
+
const commonSeparators: ReadonlySet<number> = new Set([0x2c, 0x2e, 0x2f, 0x3a, 0xa0, 0x60c, 0x202f, 0x2044]);
|
|
18
|
+
|
|
19
|
+
/** This function gives the bidi class of a character, from its code point, its script and its general category. */
|
|
20
|
+
function bidiClass(character: string): BidiClass {
|
|
21
|
+
const c = character.codePointAt(0)!;
|
|
22
|
+
if (nonSpacingMark.test(character)) return "NSM";
|
|
23
|
+
if ((c >= 0x30 && c <= 0x39) || (c >= 0x6f0 && c <= 0x6f9) || c === 0xb2 || c === 0xb3 || c === 0xb9 || (c >= 0xff10 && c <= 0xff19)) return "EN";
|
|
24
|
+
if ((c >= 0x660 && c <= 0x669) || c === 0x66b || c === 0x66c) return "AN";
|
|
25
|
+
if (c === 0x2b || c === 0x2d || c === 0x2212) return "ES";
|
|
26
|
+
if (c === 0x23 || c === 0x25 || c === 0xb0 || c === 0x2030 || currency.test(character)) return "ET";
|
|
27
|
+
if (commonSeparators.has(c)) return "CS";
|
|
28
|
+
if (c === 0x9 || c === 0xb || c === 0x1f) return "S";
|
|
29
|
+
if (c === 0xa || c === 0xd || (c >= 0x1c && c <= 0x1e) || c === 0x85 || c === 0x2029) return "B";
|
|
30
|
+
if (c === 0x20 || c === 0xc || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x3000) return "WS";
|
|
31
|
+
if (format.test(character)) return "BN";
|
|
32
|
+
if (hebrew.test(character)) return "R";
|
|
33
|
+
if (arabicLetter.test(character)) return "AL";
|
|
34
|
+
return letter.test(character) ? "L" : "ON";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** This function gives the direction of a strong class for the rule N1: `EN` and `AN` count as `R`. */
|
|
38
|
+
function strongDirection(type: BidiClass): "L" | "R" | undefined {
|
|
39
|
+
if (type === "L") return "L";
|
|
40
|
+
return type === "R" || type === "EN" || type === "AN" ? "R" : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function gives the bidi level of each code point of a text, in a paragraph with the base level 0. It applies the
|
|
45
|
+
* rules W1 to W7, N1, N2, I1 and L1 of UAX #9.
|
|
46
|
+
*/
|
|
47
|
+
export function bidiLevels(text: string): number[] {
|
|
48
|
+
const characters = [...text];
|
|
49
|
+
const original = characters.map(bidiClass);
|
|
50
|
+
const types = [...original];
|
|
51
|
+
const n = types.length;
|
|
52
|
+
// W1: a mark gets the class of the character before it
|
|
53
|
+
for (let i = 0; i < n; i++) if (types[i] === "NSM") types[i] = i === 0 || types[i - 1] === "BN" ? "L" : types[i - 1]!;
|
|
54
|
+
// W2 and W3: a European digit after an Arabic letter is an Arabic digit, and an Arabic letter is R
|
|
55
|
+
let strong: BidiClass = "L";
|
|
56
|
+
for (let i = 0; i < n; i++) {
|
|
57
|
+
const type = types[i]!;
|
|
58
|
+
if (type === "L" || type === "R" || type === "AL") strong = type;
|
|
59
|
+
else if (type === "EN" && strong === "AL") types[i] = "AN";
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i < n; i++) if (types[i] === "AL") types[i] = "R";
|
|
62
|
+
// W4: one separator between two digits of the same class gets their class
|
|
63
|
+
for (let i = 1; i + 1 < n; i++) {
|
|
64
|
+
const before = types[i - 1]!;
|
|
65
|
+
if (types[i] === "ES" && before === "EN" && types[i + 1] === "EN") types[i] = "EN";
|
|
66
|
+
else if (types[i] === "CS" && (before === "EN" || before === "AN") && types[i + 1] === before) types[i] = before;
|
|
67
|
+
}
|
|
68
|
+
// W5: a sequence of terminators next to a European digit is EN
|
|
69
|
+
for (let i = 0; i < n; i++) {
|
|
70
|
+
if (types[i] !== "ET") continue;
|
|
71
|
+
let end = i;
|
|
72
|
+
while (end < n && types[end] === "ET") end++;
|
|
73
|
+
if ((i > 0 && types[i - 1] === "EN") || (end < n && types[end] === "EN")) types.fill("EN", i, end);
|
|
74
|
+
i = end - 1;
|
|
75
|
+
}
|
|
76
|
+
// W6 and W7: the other separators and terminators are ON, and a European digit after L is L
|
|
77
|
+
for (let i = 0; i < n; i++) if (types[i] === "ES" || types[i] === "ET" || types[i] === "CS") types[i] = "ON";
|
|
78
|
+
strong = "L";
|
|
79
|
+
for (let i = 0; i < n; i++) {
|
|
80
|
+
const type = types[i]!;
|
|
81
|
+
if (type === "L" || type === "R") strong = type;
|
|
82
|
+
else if (type === "EN" && strong === "L") types[i] = "L";
|
|
83
|
+
}
|
|
84
|
+
// N1 and N2: a sequence of neutrals gets the direction of the strong characters on its two sides, or else L
|
|
85
|
+
for (let i = 0; i < n; i++) {
|
|
86
|
+
if (strongDirection(types[i]!) !== undefined) continue;
|
|
87
|
+
let end = i;
|
|
88
|
+
while (end < n && strongDirection(types[end]!) === undefined) end++;
|
|
89
|
+
const before = i === 0 ? "L" : strongDirection(types[i - 1]!)!;
|
|
90
|
+
const after = end === n ? "L" : strongDirection(types[end]!)!;
|
|
91
|
+
types.fill(before === after ? before : "L", i, end);
|
|
92
|
+
i = end - 1;
|
|
93
|
+
}
|
|
94
|
+
// I1: R goes up one level, and the digits go up two levels
|
|
95
|
+
const levels = types.map((type) => (type === "R" ? 1 : type === "AN" || type === "EN" ? 2 : 0));
|
|
96
|
+
// L1: the white space at the end and before each separator goes back to the base level
|
|
97
|
+
const isSpace = (i: number) => original[i] === "WS" || original[i] === "BN";
|
|
98
|
+
for (let i = n - 1; i >= 0 && isSpace(i); i--) levels[i] = 0;
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
if (original[i] !== "S" && original[i] !== "B") continue;
|
|
101
|
+
levels[i] = 0;
|
|
102
|
+
for (let k = i - 1; k >= 0 && isSpace(k); k--) levels[k] = 0;
|
|
103
|
+
}
|
|
104
|
+
return levels;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This function puts items in the visual order of the rule L2 of UAX #9. Each item has the level `levels[i]`. From the
|
|
109
|
+
* highest level to the level 1, it reverses each sequence of items at that level or higher.
|
|
110
|
+
*/
|
|
111
|
+
export function visualOrder<T>(items: readonly T[], levels: readonly number[]): T[] {
|
|
112
|
+
const order = items.map((_, i) => i);
|
|
113
|
+
const at = [...levels];
|
|
114
|
+
for (let level = Math.max(0, ...levels); level >= 1; level--) {
|
|
115
|
+
for (let i = 0; i < order.length; i++) {
|
|
116
|
+
if (at[i]! < level) continue;
|
|
117
|
+
let end = i;
|
|
118
|
+
while (end < order.length && at[end]! >= level) end++;
|
|
119
|
+
order.splice(i, end - i, ...order.slice(i, end).reverse());
|
|
120
|
+
at.splice(i, end - i, ...at.slice(i, end).reverse());
|
|
121
|
+
i = end;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return order.map((i) => items[i]!);
|
|
125
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** A big-endian reader over a part of a font file. The offsets are relative to the start of the part. */
|
|
2
|
+
export class Reader {
|
|
3
|
+
readonly bytes: Uint8Array;
|
|
4
|
+
private readonly view: DataView;
|
|
5
|
+
|
|
6
|
+
constructor(bytes: Uint8Array) {
|
|
7
|
+
this.bytes = bytes;
|
|
8
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get length(): number {
|
|
12
|
+
return this.bytes.byteLength;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
u8(offset: number): number {
|
|
16
|
+
return this.view.getUint8(offset);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
u16(offset: number): number {
|
|
20
|
+
return this.view.getUint16(offset);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
i16(offset: number): number {
|
|
24
|
+
return this.view.getInt16(offset);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
u32(offset: number): number {
|
|
28
|
+
return this.view.getUint32(offset);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
tag(offset: number): string {
|
|
32
|
+
return String.fromCharCode(this.u8(offset), this.u8(offset + 1), this.u8(offset + 2), this.u8(offset + 3));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** This method gives a reader over the bytes from `offset`, with the length `length` or to the end. */
|
|
36
|
+
part(offset: number, length?: number): Reader {
|
|
37
|
+
const end = length === undefined ? this.bytes.byteLength : offset + length;
|
|
38
|
+
if (offset < 0 || end > this.bytes.byteLength || offset > end) throw new RangeError(`the part ${offset}..${end} is outside ${this.bytes.byteLength} bytes`);
|
|
39
|
+
return new Reader(this.bytes.subarray(offset, end));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules of one text engine for the widths of a font. The fields `deltas` and `metrics` are options of `readFont`, and
|
|
3
|
+
* the fields `rounding` and `sum` are options of `measureWidth`. Thus a caller spreads the same row into the two calls.
|
|
4
|
+
*/
|
|
5
|
+
export interface EngineRules {
|
|
6
|
+
/** The rounding of the `HVAR` and `gvar` deltas of an instance. */
|
|
7
|
+
readonly deltas: "round" | "exact";
|
|
8
|
+
/** The advance of a composite glyph: its own advance, or the advance of its component with the flag USE_MY_METRICS. */
|
|
9
|
+
readonly metrics: "own" | "component";
|
|
10
|
+
/** The rounding of each advance: `pixel` rounds it to whole px, and `exact` keeps the fraction. */
|
|
11
|
+
readonly rounding: "exact" | "pixel";
|
|
12
|
+
/**
|
|
13
|
+
* The sum of the advances: `exact`, or `float32` as Blink adds the widths of the words of a canvas in single precision.
|
|
14
|
+
* A sum of whole px is exact in single precision, so the rounding `pixel` needs no `float32` sum.
|
|
15
|
+
*/
|
|
16
|
+
readonly sum: "exact" | "float32";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The rules of the text engines of Chromium, one row for each engine. The fonts runs of typeset#27 measured each row
|
|
21
|
+
* against the canvas, and the T1d diagnosis of the sum measured the field `sum`. A new engine is a new row.
|
|
22
|
+
*/
|
|
23
|
+
export const engines: { readonly freetype: EngineRules; readonly coretext: EngineRules; readonly directwrite: EngineRules } = {
|
|
24
|
+
/** FreeType, as Chromium on Linux uses it: rounded deltas, and each advance in whole px. */
|
|
25
|
+
freetype: { deltas: "round", metrics: "own", rounding: "pixel", sum: "exact" },
|
|
26
|
+
/** CoreText on macOS: it keeps the fraction of the deltas. */
|
|
27
|
+
coretext: { deltas: "exact", metrics: "own", rounding: "exact", sum: "float32" },
|
|
28
|
+
/** DirectWrite on Windows: a composite glyph has the advance of its USE_MY_METRICS component. */
|
|
29
|
+
directwrite: { deltas: "round", metrics: "component", rounding: "exact", sum: "float32" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The name of a text engine of the table `engines`. */
|
|
33
|
+
export type EngineName = keyof typeof engines;
|
|
34
|
+
|
|
35
|
+
/** This function tells if a text is the name of an engine of the table `engines`. */
|
|
36
|
+
export function isEngineName(name: string): name is EngineName {
|
|
37
|
+
return Object.hasOwn(engines, name);
|
|
38
|
+
}
|