@mark1russell7/typeset 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +21 -1
  2. package/build/chunks/engines.d.ts +32 -0
  3. package/build/chunks/engines.d.ts.map +1 -0
  4. package/build/chunks/engines.js +35 -0
  5. package/build/chunks/engines.js.map +1 -0
  6. package/build/chunks/font.d.ts +228 -0
  7. package/build/chunks/font.d.ts.map +1 -0
  8. package/build/chunks/font.js +3779 -0
  9. package/build/chunks/font.js.map +1 -0
  10. package/build/chunks/fonts.d.ts +31 -0
  11. package/build/chunks/fonts.d.ts.map +1 -0
  12. package/build/chunks/fonts.js +150 -0
  13. package/build/chunks/fonts.js.map +1 -0
  14. package/build/chunks/measurer.d.ts +67 -0
  15. package/build/chunks/measurer.d.ts.map +1 -0
  16. package/build/chunks/measurer.js +277 -0
  17. package/build/chunks/measurer.js.map +1 -0
  18. package/build/chunks/sfnt.d.ts +22 -0
  19. package/build/chunks/sfnt.d.ts.map +1 -0
  20. package/build/chunks/text.js.map +1 -1
  21. package/build/fonts-inspect.d.ts +10 -0
  22. package/build/fonts-inspect.d.ts.map +1 -0
  23. package/build/fonts-inspect.js +1 -0
  24. package/build/fonts.d.ts +5 -0
  25. package/build/fonts.js +4 -0
  26. package/build/index.d.ts +3 -1
  27. package/build/index.js +3 -1
  28. package/build/measurer.d.ts +2 -0
  29. package/build/measurer.js +1 -0
  30. package/package.json +18 -4
  31. package/src/fonts/bidi.ts +125 -0
  32. package/src/fonts/binary.ts +41 -0
  33. package/src/fonts/bound.ts +167 -0
  34. package/src/fonts/engines.ts +38 -0
  35. package/src/fonts/font.ts +458 -0
  36. package/src/fonts/gvar.ts +220 -0
  37. package/src/fonts/index.ts +11 -0
  38. package/src/fonts/indic.ts +626 -0
  39. package/src/fonts/joining.ts +141 -0
  40. package/src/fonts/kerning.ts +278 -0
  41. package/src/fonts/sfnt.ts +102 -0
  42. package/src/fonts/substitution.ts +686 -0
  43. package/src/fonts/tables.ts +162 -0
  44. package/src/fonts/variations.ts +175 -0
  45. package/src/fonts/words.ts +119 -0
  46. package/src/fonts-inspect.ts +8 -0
  47. package/src/fonts.ts +9 -0
  48. package/src/index.ts +2 -0
  49. package/src/measurer/advance.ts +307 -0
  50. package/src/measurer.ts +5 -0
  51. package/src/text/rope.ts +1 -1
@@ -0,0 +1,162 @@
1
+ import type { Reader } from "./binary.ts";
2
+
3
+ /** The fields of `head`, `hhea` and `maxp` that the width uses. */
4
+ export interface Header {
5
+ readonly unitsPerEm: number;
6
+ /** The revision of the font, as the raw 16.16 number. */
7
+ readonly revision: number;
8
+ readonly checksumAdjustment: number;
9
+ readonly numberOfHMetrics: number;
10
+ readonly numGlyphs: number;
11
+ }
12
+
13
+ /** This function reads the header fields from `head`, `hhea` and `maxp`. */
14
+ export function readHeader(head: Reader, hhea: Reader, maxp: Reader): Header {
15
+ if (head.u32(12) !== 0x5f0f3cf5) throw new Error("the head table has a bad magic number");
16
+ return { revision: head.u32(4), checksumAdjustment: head.u32(8), unitsPerEm: head.u16(18), numberOfHMetrics: hhea.u16(34), numGlyphs: maxp.u16(4) };
17
+ }
18
+
19
+ /** This function gives the advance of each glyph in font units, from `hmtx`. */
20
+ export function readAdvances(hmtx: Reader, header: Header): Uint16Array {
21
+ const advances = new Uint16Array(header.numGlyphs);
22
+ let last = 0;
23
+ for (let glyph = 0; glyph < header.numGlyphs; glyph++) {
24
+ if (glyph < header.numberOfHMetrics) last = hmtx.u16(glyph * 4);
25
+ advances[glyph] = last;
26
+ }
27
+ return advances;
28
+ }
29
+
30
+ /** This function gives the map from code points to glyphs of `cmap`. It uses a format 12 subtable if there is one, else a format 4 subtable. */
31
+ export function readCharacterMap(cmap: Reader): Map<number, number> {
32
+ const count = cmap.u16(2);
33
+ let best: { rank: number; offset: number } | undefined;
34
+ for (let i = 0; i < count; i++) {
35
+ const record = 4 + i * 8;
36
+ const platform = cmap.u16(record);
37
+ const encoding = cmap.u16(record + 2);
38
+ const offset = cmap.u32(record + 4);
39
+ const format = cmap.u16(offset);
40
+ const unicode = platform === 0 || (platform === 3 && (encoding === 1 || encoding === 10));
41
+ if (!unicode) continue;
42
+ const rank = format === 12 ? 2 : format === 4 ? 1 : 0;
43
+ if (rank > 0 && (best === undefined || rank > best.rank)) best = { rank, offset };
44
+ }
45
+ if (best === undefined) throw new Error("the cmap table has no Unicode subtable of format 4 or 12");
46
+ const table = cmap.part(best.offset);
47
+ const map = new Map<number, number>();
48
+ if (best.rank === 2) {
49
+ const groups = table.u32(12);
50
+ for (let g = 0; g < groups; g++) {
51
+ const at = 16 + g * 12;
52
+ const start = table.u32(at);
53
+ const end = table.u32(at + 4);
54
+ const glyph = table.u32(at + 8);
55
+ for (let c = start; c <= end; c++) map.set(c, glyph + c - start);
56
+ }
57
+ return map;
58
+ }
59
+ const segments = table.u16(6) / 2;
60
+ const ends = 14;
61
+ const starts = ends + segments * 2 + 2;
62
+ const deltas = starts + segments * 2;
63
+ const ranges = deltas + segments * 2;
64
+ for (let s = 0; s < segments; s++) {
65
+ const start = table.u16(starts + s * 2);
66
+ const end = table.u16(ends + s * 2);
67
+ const delta = table.u16(deltas + s * 2);
68
+ const range = table.u16(ranges + s * 2);
69
+ for (let c = start; c <= end && c !== 0xffff; c++) {
70
+ let glyph: number;
71
+ if (range === 0) glyph = (c + delta) & 0xffff;
72
+ else {
73
+ glyph = table.u16(ranges + s * 2 + range + (c - start) * 2);
74
+ if (glyph !== 0) glyph = (glyph + delta) & 0xffff;
75
+ }
76
+ if (glyph !== 0) map.set(c, glyph);
77
+ }
78
+ }
79
+ return map;
80
+ }
81
+
82
+ /** This function gives the class of a glyph in a ClassDef table. A glyph that the table does not list has the class 0. */
83
+ export function classOf(table: Reader, glyph: number): number {
84
+ const format = table.u16(0);
85
+ if (format === 1) {
86
+ const start = table.u16(2);
87
+ const count = table.u16(4);
88
+ return glyph >= start && glyph < start + count ? table.u16(6 + (glyph - start) * 2) : 0;
89
+ }
90
+ if (format === 2) {
91
+ let low = 0;
92
+ let high = table.u16(2) - 1;
93
+ while (low <= high) {
94
+ const middle = (low + high) >> 1;
95
+ const at = 4 + middle * 6;
96
+ if (glyph < table.u16(at)) high = middle - 1;
97
+ else if (glyph > table.u16(at + 2)) low = middle + 1;
98
+ else return table.u16(at + 4);
99
+ }
100
+ return 0;
101
+ }
102
+ throw new Error(`the ClassDef format ${format} is unknown`);
103
+ }
104
+
105
+ /** This function gives the index of a glyph in a Coverage table, or -1 when the table does not cover the glyph. */
106
+ export function coverageIndex(table: Reader, glyph: number): number {
107
+ const format = table.u16(0);
108
+ let low = 0;
109
+ let high = table.u16(2) - 1;
110
+ if (format === 1) {
111
+ while (low <= high) {
112
+ const middle = (low + high) >> 1;
113
+ const value = table.u16(4 + middle * 2);
114
+ if (glyph < value) high = middle - 1;
115
+ else if (glyph > value) low = middle + 1;
116
+ else return middle;
117
+ }
118
+ return -1;
119
+ }
120
+ if (format === 2) {
121
+ while (low <= high) {
122
+ const middle = (low + high) >> 1;
123
+ const at = 4 + middle * 6;
124
+ if (glyph < table.u16(at)) high = middle - 1;
125
+ else if (glyph > table.u16(at + 2)) low = middle + 1;
126
+ else return table.u16(at + 4) + glyph - table.u16(at);
127
+ }
128
+ return -1;
129
+ }
130
+ throw new Error(`the Coverage format ${format} is unknown`);
131
+ }
132
+
133
+ /** This function gives each glyph of a Coverage table with its coverage index, in the order of the table. */
134
+ export function coverageGlyphs(table: Reader): (readonly [glyph: number, index: number])[] {
135
+ const format = table.u16(0);
136
+ const count = table.u16(2);
137
+ const glyphs: (readonly [number, number])[] = [];
138
+ if (format === 1) {
139
+ for (let k = 0; k < count; k++) glyphs.push([table.u16(4 + k * 2), k]);
140
+ return glyphs;
141
+ }
142
+ if (format === 2) {
143
+ for (let k = 0; k < count; k++) {
144
+ const at = 4 + k * 6;
145
+ const start = table.u16(at);
146
+ const end = table.u16(at + 2);
147
+ const index = table.u16(at + 4);
148
+ for (let glyph = start; glyph <= end; glyph++) glyphs.push([glyph, index + glyph - start]);
149
+ }
150
+ return glyphs;
151
+ }
152
+ throw new Error(`the Coverage format ${format} is unknown`);
153
+ }
154
+
155
+ /** The GDEF glyph classes: 1 base, 2 ligature, 3 mark, 4 component. */
156
+ export function readGlyphClasses(gdef: Reader | undefined): ((glyph: number) => number) {
157
+ if (gdef === undefined) return () => 0;
158
+ const offset = gdef.u16(4);
159
+ if (offset === 0) return () => 0;
160
+ const table = gdef.part(offset);
161
+ return (glyph) => classOf(table, glyph);
162
+ }
@@ -0,0 +1,175 @@
1
+ import type { Reader } from "./binary.ts";
2
+
3
+ /** One axis of `fvar`, in the user units of the axis. */
4
+ export interface Axis {
5
+ readonly tag: string;
6
+ readonly min: number;
7
+ readonly default: number;
8
+ readonly max: number;
9
+ }
10
+
11
+ /** The coordinates of an instance in user units, by axis tag. An axis that is absent has its default value. */
12
+ export type Instance = Readonly<Record<string, number>>;
13
+
14
+ /** This function reads a 16.16 fixed number. */
15
+ function fixed(table: Reader, offset: number): number {
16
+ return (table.u16(offset) << 16 >> 16) + table.u16(offset + 2) / 65536;
17
+ }
18
+
19
+ /** This function reads a 2.14 fixed number. */
20
+ function f2dot14(table: Reader, offset: number): number {
21
+ return table.i16(offset) / 16384;
22
+ }
23
+
24
+ /** This function rounds a number to the 2.14 grid, as HarfBuzz and FreeType do with each normalized coordinate. */
25
+ function toF2dot14(value: number): number {
26
+ return Math.round(value * 16384) / 16384;
27
+ }
28
+
29
+ /** This function gives the axes of `fvar`. */
30
+ export function readAxes(fvar: Reader): Axis[] {
31
+ const offset = fvar.u16(4);
32
+ const count = fvar.u16(8);
33
+ const size = fvar.u16(10);
34
+ const axes: Axis[] = [];
35
+ for (let i = 0; i < count; i++) {
36
+ const at = offset + i * size;
37
+ axes.push({ tag: fvar.tag(at), min: fixed(fvar, at + 4), default: fixed(fvar, at + 8), max: fixed(fvar, at + 12) });
38
+ }
39
+ return axes;
40
+ }
41
+
42
+ /**
43
+ * This function gives the normalized coordinates of an instance. Each coordinate is in -1 to 1, on the 2.14 grid. The
44
+ * segment maps of `avar` change it when the font has them. A value outside the range of an axis is clamped to the range.
45
+ */
46
+ export function normalize(axes: readonly Axis[], avar: Reader | undefined, instance: Instance): number[] {
47
+ const coordinates = axes.map((axis) => {
48
+ const value = Math.min(axis.max, Math.max(axis.min, instance[axis.tag] ?? axis.default));
49
+ if (value < axis.default) return toF2dot14(axis.default === axis.min ? 0 : (value - axis.default) / (axis.default - axis.min));
50
+ if (value > axis.default) return toF2dot14(axis.max === axis.default ? 0 : (value - axis.default) / (axis.max - axis.default));
51
+ return 0;
52
+ });
53
+ if (avar === undefined || avar.u16(0) !== 1) return coordinates;
54
+ let at = 8;
55
+ const count = Math.min(avar.u16(6), axes.length);
56
+ for (let a = 0; a < count; a++) {
57
+ const pairs = avar.u16(at);
58
+ const from: number[] = [];
59
+ const to: number[] = [];
60
+ for (let p = 0; p < pairs; p++) {
61
+ from.push(f2dot14(avar, at + 2 + p * 4));
62
+ to.push(f2dot14(avar, at + 4 + p * 4));
63
+ }
64
+ at += 2 + pairs * 4;
65
+ coordinates[a] = toF2dot14(mapSegment(from, to, coordinates[a]!));
66
+ }
67
+ return coordinates;
68
+ }
69
+
70
+ /** This function maps a coordinate through one segment map of `avar`, by linear interpolation between its points. */
71
+ function mapSegment(from: readonly number[], to: readonly number[], value: number): number {
72
+ if (from.length === 0) return value;
73
+ if (value <= from[0]!) return to[0]! + value - from[0]!;
74
+ for (let i = 1; i < from.length; i++) {
75
+ if (value === from[i]) return to[i]!;
76
+ if (value < from[i]!) return to[i - 1]! + ((value - from[i - 1]!) * (to[i]! - to[i - 1]!)) / (from[i]! - from[i - 1]!);
77
+ }
78
+ return to[to.length - 1]! + value - from[from.length - 1]!;
79
+ }
80
+
81
+ /** An ItemVariationStore at fixed coordinates. */
82
+ export interface VariationStore {
83
+ /** This function gives the delta of the item `inner` of the data `outer`, in font units, not rounded. */
84
+ delta(outer: number, inner: number): number;
85
+ }
86
+
87
+ /** This function reads an ItemVariationStore and computes the scalar of each region at the coordinates. */
88
+ export function readVariationStore(store: Reader, coordinates: readonly number[]): VariationStore {
89
+ const regions = store.part(store.u32(2));
90
+ const axisCount = regions.u16(0);
91
+ const regionCount = regions.u16(2);
92
+ const scalars: number[] = [];
93
+ for (let r = 0; r < regionCount; r++) {
94
+ let scalar = 1;
95
+ for (let a = 0; a < axisCount && scalar !== 0; a++) {
96
+ const at = 4 + (r * axisCount + a) * 6;
97
+ scalar *= axisScalar(f2dot14(regions, at), f2dot14(regions, at + 2), f2dot14(regions, at + 4), coordinates[a] ?? 0);
98
+ }
99
+ scalars.push(scalar);
100
+ }
101
+ const dataCount = store.u16(6);
102
+ const data = Array.from({ length: dataCount }, (_, i) => store.part(store.u32(8 + i * 4)));
103
+ return {
104
+ delta(outer, inner) {
105
+ const table = data[outer];
106
+ if (table === undefined || inner >= table.u16(0)) return 0;
107
+ const wordField = table.u16(2);
108
+ const longWords = (wordField & 0x8000) !== 0;
109
+ const words = wordField & 0x7fff;
110
+ const count = table.u16(4);
111
+ const wordSize = longWords ? 4 : 2;
112
+ const smallSize = longWords ? 2 : 1;
113
+ const rowSize = words * wordSize + (count - words) * smallSize;
114
+ let at = 6 + count * 2 + inner * rowSize;
115
+ let sum = 0;
116
+ for (let i = 0; i < count; i++) {
117
+ let value: number;
118
+ if (i < words) {
119
+ value = longWords ? table.u32(at) | 0 : table.i16(at);
120
+ at += wordSize;
121
+ } else {
122
+ value = longWords ? table.i16(at) : (table.u8(at) << 24) >> 24;
123
+ at += smallSize;
124
+ }
125
+ const scalar = scalars[table.u16(6 + i * 2)] ?? 0;
126
+ if (scalar !== 0) sum += scalar * value;
127
+ }
128
+ return sum;
129
+ },
130
+ };
131
+ }
132
+
133
+ /** This function gives the scalar of one axis of a region, as the OpenType specification defines it. */
134
+ export function axisScalar(start: number, peak: number, end: number, value: number): number {
135
+ if (start > peak || peak > end) return 1;
136
+ if (start < 0 && end > 0 && peak !== 0) return 1;
137
+ if (peak === 0 || value === peak) return 1;
138
+ if (value <= start || value >= end) return 0;
139
+ return value < peak ? (value - start) / (peak - start) : (end - value) / (end - peak);
140
+ }
141
+
142
+ /** This function gives the outer and inner index of a glyph in a DeltaSetIndexMap. A glyph after the end uses the last entry. */
143
+ export function mapIndex(map: Reader, index: number): [outer: number, inner: number] {
144
+ const format = map.u8(0);
145
+ const entryFormat = map.u8(1);
146
+ const count = format === 0 ? map.u16(2) : map.u32(2);
147
+ const start = format === 0 ? 4 : 6;
148
+ if (count === 0) return [0, index];
149
+ const size = ((entryFormat >> 4) & 3) + 1;
150
+ const innerBits = (entryFormat & 0xf) + 1;
151
+ const at = start + Math.min(index, count - 1) * size;
152
+ let entry = 0;
153
+ for (let i = 0; i < size; i++) entry = entry * 256 + map.u8(at + i);
154
+ const innerCount = 2 ** innerBits;
155
+ return [Math.floor(entry / innerCount), entry % innerCount];
156
+ }
157
+
158
+ /**
159
+ * This function gives the advances of an instance. With `round`, each delta of `HVAR` is rounded to whole font units, as
160
+ * FreeType rounds it: a half goes up. With `exact`, the delta keeps its fraction. The advances stay as they are when the
161
+ * font has no `HVAR` table.
162
+ */
163
+ export function instanceAdvances(advances: Uint16Array, hvar: Reader | undefined, coordinates: readonly number[], deltas: "round" | "exact" = "round"): Float64Array {
164
+ const result = Float64Array.from(advances);
165
+ if (hvar === undefined || coordinates.every((value) => value === 0)) return result;
166
+ const store = readVariationStore(hvar.part(hvar.u32(4)), coordinates);
167
+ const mapOffset = hvar.u32(8);
168
+ const map = mapOffset === 0 ? undefined : hvar.part(mapOffset);
169
+ for (let glyph = 0; glyph < result.length; glyph++) {
170
+ const [outer, inner] = map === undefined ? [0, glyph] : mapIndex(map, glyph);
171
+ const delta = store.delta(outer, inner);
172
+ result[glyph] = advances[glyph]! + (deltas === "round" ? Math.round(delta) : delta);
173
+ }
174
+ return result;
175
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The words of the text of a canvas. Blink shapes the text of `fillText` and `measureText` one word at a time, so no GSUB
3
+ * context and no pair kerning goes across two words. This module follows `NextWordEndIndex` of
4
+ * `third_party/blink/renderer/platform/fonts/plain_text_node.cc` and the CJK table of `character_property_data.h`.
5
+ * The Chromium Authors wrote these files under a BSD-style license.
6
+ */
7
+ import { bidiLevels, visualOrder } from "./bidi.ts";
8
+
9
+ /** The single code points of the CJK table of Blink. */
10
+ const cjkValues: ReadonlySet<number> = new Set([
11
+ 0x2c7, 0x2ca, 0x2cb, 0x2d9, 0x2020, 0x2021, 0x2030, 0x203b, 0x203c, 0x2042, 0x2047, 0x2048, 0x2049, 0x2051, 0x20dd, 0x20de, 0x2100, 0x2103,
12
+ 0x2105, 0x2109, 0x210a, 0x2113, 0x2116, 0x2121, 0x212b, 0x213b, 0x2150, 0x2151, 0x2152, 0x217f, 0x2189, 0x2307, 0x2312, 0x23ce, 0x2423, 0x25a0,
13
+ 0x25a1, 0x25a2, 0x25aa, 0x25ab, 0x25b1, 0x25b2, 0x25b3, 0x25b6, 0x25b7, 0x25bc, 0x25bd, 0x25c0, 0x25c1, 0x25c6, 0x25c7, 0x25c9, 0x25cb, 0x25cc,
14
+ 0x25ef, 0x2605, 0x2606, 0x260e, 0x2616, 0x2617, 0x26a0, 0x2713, 0x271a, 0x273f, 0x2740, 0x2756, 0x2763, 0x2b1a, 0xfe10, 0xfe11, 0xfe12, 0xfe19,
15
+ 0xff1d, 0x1f100, 0x1f200, 0x1f237, 0x1f32c, 0x1f336, 0x1f37d, 0x1f43f, 0x1f54f, 0x1f93b, 0x1f946,
16
+ ]);
17
+
18
+ /** The ranges of the CJK table of Blink, as pairs of the first and the last code point. */
19
+ const cjkRanges: readonly number[] = [
20
+ 0x2e80, 0x2fdf, 0x31c0, 0x31ef, 0x3400, 0x4dbf, 0x4e00, 0x9fff, 0xf900, 0xfaff, 0x20000, 0x2ffff, 0x2156, 0x215a, 0x2160, 0x216b, 0x2170, 0x217b,
21
+ 0x23be, 0x23cc, 0x2460, 0x2492, 0x249c, 0x24ff, 0x25ce, 0x25d3, 0x25e2, 0x25e6, 0x2600, 0x2603, 0x2660, 0x266f, 0x2672, 0x267d, 0x2776, 0x277f,
22
+ 0x2ff0, 0x302d, 0x3031, 0x312f, 0x3190, 0x31bf, 0x3200, 0x33ff, 0x4dc0, 0x4dff, 0xf860, 0xf862, 0xfe30, 0xfe6f, 0xff00, 0xff0c, 0xff0e, 0xff1a,
23
+ 0xff1f, 0xffef, 0x16fe0, 0x16fff, 0x17000, 0x187ff, 0x18800, 0x18aff, 0x1b000, 0x1b0ff, 0x1b100, 0x1b12f, 0x1b170, 0x1b2ff, 0x1f110, 0x1f129,
24
+ 0x1f130, 0x1f149, 0x1f150, 0x1f169, 0x1f170, 0x1f189, 0x1f202, 0x1f219, 0x1f21b, 0x1f22e, 0x1f230, 0x1f231, 0x1f23b, 0x1f24f, 0x1f252, 0x1f2ff,
25
+ 0x1f321, 0x1f32a, 0x1f394, 0x1f39f, 0x1f3cd, 0x1f3ce, 0x1f3d4, 0x1f3df, 0x1f3f1, 0x1f3f2, 0x1f3f5, 0x1f3f7, 0x1f4fd, 0x1f4fe, 0x1f53e, 0x1f54a,
26
+ 0x1f568, 0x1f573, 0x1f576, 0x1f579, 0x1f57b, 0x1f58f, 0x1f591, 0x1f594, 0x1f597, 0x1f5a3, 0x1f5a5, 0x1f5e7, 0x1f5e9, 0x1f5fa, 0x1f650, 0x1f67f,
27
+ 0x1f6c6, 0x1f6cb, 0x1f6cd, 0x1f6cf, 0x1f6d3, 0x1f6d4, 0x1f6d9, 0x1f6db, 0x1f6e0, 0x1f6ea, 0x1f6ed, 0x1f6f3, 0x1f6fd, 0x1f6ff, 0x1f900, 0x1f90b,
28
+ 0x1fac9, 0x1facc,
29
+ ];
30
+
31
+ const emojiPresentation = /\p{Emoji_Presentation}/u;
32
+ const modifierLike = /[\p{M}\p{Lm}\p{Sk}]/u;
33
+ const continuesEmoji = /[‍\p{Emoji_Component}\p{Extended_Pictographic}]/u;
34
+ const commonOrInherited = /[\p{Script=Common}\p{Script=Inherited}]/u;
35
+
36
+ /**
37
+ * This function tells if Blink starts a CJK word at the code point `codePoint`. Blink also marks the pictographs of the
38
+ * RGI emoji sequences. This function uses `Emoji_Presentation` for them, so some text-style pictographs are missing.
39
+ */
40
+ export function isCjkIdeographOrSymbol(codePoint: number): boolean {
41
+ if (codePoint < 0x2c7) return false;
42
+ if (cjkValues.has(codePoint)) return true;
43
+ for (let i = 0; i < cjkRanges.length; i += 2) if (codePoint >= cjkRanges[i]! && codePoint <= cjkRanges[i + 1]!) return true;
44
+ return emojiPresentation.test(String.fromCodePoint(codePoint));
45
+ }
46
+
47
+ /** This function tells if Blink ends a word before the character `character`: a space, a tab or a zero width space. */
48
+ function isDelimiter(character: string): boolean {
49
+ return character === " " || character === "\t" || character === "​";
50
+ }
51
+
52
+ /**
53
+ * This function divides the text of a canvas into the words that Blink shapes one at a time. Each space, tab and zero
54
+ * width space is a word alone. Each CJK character starts a word, with its marks and the CJK characters of the script
55
+ * Common after it.
56
+ */
57
+ export function canvasWords(text: string): string[] {
58
+ const characters = [...text];
59
+ const words: string[] = [];
60
+ let start = 0;
61
+ while (start < characters.length) {
62
+ const end = wordEnd(characters, start);
63
+ words.push(characters.slice(start, end).join(""));
64
+ start = end;
65
+ }
66
+ return words;
67
+ }
68
+
69
+ /**
70
+ * This function gives the segments of the text of a canvas in the visual order. A segment is a word of `canvasWords`, or a
71
+ * part of a word where the bidi level changes. Blink shapes each bidi run alone and adds the widths of the words in this order.
72
+ */
73
+ export function canvasSegments(text: string): string[] {
74
+ const characters = [...text];
75
+ const levels = bidiLevels(text);
76
+ const segments: string[] = [];
77
+ const segmentLevels: number[] = [];
78
+ let at = 0;
79
+ for (const word of canvasWords(text)) {
80
+ const end = at + [...word].length;
81
+ let start = at;
82
+ for (let i = at + 1; i <= end; i++) {
83
+ if (i < end && levels[i] === levels[start]) continue;
84
+ segments.push(characters.slice(start, i).join(""));
85
+ segmentLevels.push(levels[start]!);
86
+ start = i;
87
+ }
88
+ at = end;
89
+ }
90
+ return visualOrder(segments, segmentLevels);
91
+ }
92
+
93
+ /** This function gives the index after the last character of the word that starts at `start`. */
94
+ function wordEnd(characters: readonly string[], start: number): number {
95
+ const first = characters[start]!;
96
+ if (start + 1 === characters.length || isDelimiter(first)) return start + 1;
97
+ let end = start + 1;
98
+ if (!isCjkIdeographOrSymbol(first.codePointAt(0)!)) {
99
+ for (; end < characters.length; end++) {
100
+ const character = characters[end]!;
101
+ if (isDelimiter(character) || (isCjkIdeographOrSymbol(character.codePointAt(0)!) && !modifierLike.test(character))) return end;
102
+ }
103
+ return end;
104
+ }
105
+ let hasScript = !commonOrInherited.test(first);
106
+ for (; end < characters.length; end++) {
107
+ const character = characters[end]!;
108
+ if (modifierLike.test(character) || continuesEmoji.test(character)) continue;
109
+ if (isCjkIdeographOrSymbol(character.codePointAt(0)!)) {
110
+ if (commonOrInherited.test(character)) continue;
111
+ if (!hasScript) {
112
+ hasScript = true;
113
+ continue;
114
+ }
115
+ }
116
+ return end;
117
+ }
118
+ return end;
119
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The subpath `./fonts/inspect`: the parts of the font model that a tool uses to examine a width. They give the glyphs,
3
+ * the shaping words, the default features and the tables of a font file.
4
+ */
5
+ export { glyphAdvances, shapingWords } from "./fonts/font.ts";
6
+ export { canvasWords } from "./fonts/words.ts";
7
+ export { defaultFeatures } from "./fonts/substitution.ts";
8
+ export { readTables } from "./fonts/sfnt.ts";
package/src/fonts.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The subpath `./fonts`: the font model of typeset. It reads the tables of a TrueType, OpenType or WOFF2 file. It gives
3
+ * the width of a text as the text engine of a browser gives it. The host gives the bytes of the font files.
4
+ */
5
+ export { measureWidth, readFont, type Font, type MeasureOptions, type ReadOptions } from "./fonts/font.ts";
6
+ export { widthBound, type WidthBound } from "./fonts/bound.ts";
7
+ export { engines, isEngineName, type EngineName, type EngineRules } from "./fonts/engines.ts";
8
+ export type { Decompress } from "./fonts/sfnt.ts";
9
+ export type { Axis, Instance } from "./fonts/variations.ts";
package/src/index.ts CHANGED
@@ -3,4 +3,6 @@
3
3
  * `@mark1russell7/typeset/breaking`. This entry gives each area as one namespace.
4
4
  */
5
5
  export * as breaking from "./breaking.ts";
6
+ export * as fonts from "./fonts.ts";
7
+ export * as measurer from "./measurer.ts";
6
8
  export * as text from "./text.ts";