@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
@@ -1 +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 `@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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"}
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 { o as canvasWords, u as defaultFeatures, t as glyphAdvances, d as readTables, a as shapingWords } from "./chunks/font.js";
@@ -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 { n as WidthBound, r as widthBound } from "./chunks/fonts.js";
4
+ import { i as isEngineName, n as EngineRules, r as engines, t as EngineName } from "./chunks/engines.js";
5
+ export { type Axis, type Decompress, type EngineName, type EngineRules, type Font, type Instance, type MeasureOptions, type ReadOptions, type WidthBound, engines, isEngineName, measureWidth, readFont, widthBound };
package/build/fonts.js ADDED
@@ -0,0 +1,4 @@
1
+ import { n as measureWidth, r as readFont } from "./chunks/font.js";
2
+ import { n as widthBound } from "./chunks/fonts.js";
3
+ import { n as isEngineName, t as engines } from "./chunks/engines.js";
4
+ export { engines, isEngineName, measureWidth, readFont, widthBound };
package/build/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  import { t as breaking_d_exports } from "./chunks/breaking.js";
2
+ import { t as fonts_d_exports } from "./chunks/fonts.js";
3
+ import { t as measurer_d_exports } from "./chunks/measurer.js";
2
4
  import { t as text_d_exports } from "./chunks/text.js";
3
- export { breaking_d_exports as breaking, text_d_exports as text };
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,3 +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";
2
4
  import { t as text_exports } from "./chunks/text.js";
3
- export { breaking_exports as breaking, text_exports as text };
5
+ export { breaking_exports as breaking, fonts_exports as fonts, measurer_exports as measurer, text_exports as text };
@@ -0,0 +1,2 @@
1
+ import { a as fontAdvance, i as FontSpec, n as FontAdvanceOptions, o as parseFont, r as FontFace, s as scriptTag } from "./chunks/measurer.js";
2
+ export { type FontAdvanceOptions, type FontFace, type FontSpec, fontAdvance, parseFont, scriptTag };
@@ -0,0 +1 @@
1
+ export { n as fontAdvance, r as parseFont, i as scriptTag } from "./chunks/measurer.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.1",
5
- "description": "Text measurement and line breaking. The subpath ./breaking has the line breakers, and ./text has a rope with the width of each part, for any width function.",
4
+ "version": "0.1.3",
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,18 @@
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
+ },
37
49
  "./text": {
38
50
  "types": "./build/text.d.ts",
39
51
  "default": "./build/text.js"
@@ -52,10 +64,12 @@
52
64
  "publishConfig": {
53
65
  "access": "public"
54
66
  },
67
+ "dependencies": {
68
+ "@mark1russell7/rigor-generators": "^0.1.0"
69
+ },
55
70
  "devDependencies": {
56
- "@mark1russell7/cue": "^1.0.2",
71
+ "@mark1russell7/cue": "^2.0.0",
57
72
  "@mark1russell7/layout-contract": "^0.3.2",
58
- "@mark1russell7/rigor-generators": "^0.1.0",
59
73
  "@types/node": "^26.6.5",
60
74
  "@typeset/corpus": "0.1.0",
61
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,167 @@
1
+ import { shapingOf, type Font, type MeasureOptions } from "./font.ts";
2
+ import { indicFeatures, indicForms } from "./indic.ts";
3
+ import { joiningStages } from "./joining.ts";
4
+ import type { SubstitutionRule } from "./substitution.ts";
5
+
6
+ /** The limit of `widthBound` for the width of a text over a declared set of characters. It needs no measurement. */
7
+ export interface WidthBound {
8
+ /** The largest total advance of one character with all its expansions, in font units. */
9
+ readonly advance: number;
10
+ /** The largest positive kerning of one glyph, in font units. */
11
+ readonly kerning: number;
12
+ /** The largest count of glyphs of one character. */
13
+ readonly glyphs: number;
14
+ /**
15
+ * This function gives the limit in px for a text of `length` UTF-16 units at the size `size`. It takes the length of a
16
+ * string, so it does not read the text. The limit is for a text with only the characters of the set.
17
+ */
18
+ width(length: number, size: number): number;
19
+ }
20
+
21
+ /**
22
+ * The largest count of passes over the lookups of a shaper with stages. A pass covers one stage, and no shaper has more stages:
23
+ * the Indic shaper has 13 and the joining has 11. A rule that expands its own glyph has no fixed point, so it stops at this count.
24
+ */
25
+ const ROUNDS = 16;
26
+ /** The relative error of one rounding to single precision. */
27
+ const FLOAT32 = 2 ** -24;
28
+ /** The relative error of one rounding to double precision. */
29
+ const FLOAT64 = 2 ** -53;
30
+ /** The largest size of a kerning value of a `ValueRecord`, and of its variation delta, in font units: the range of `int16`. */
31
+ const KERNING_RANGE = 2 ** 15;
32
+
33
+ /** The largest expansion of a glyph: the sum of its advances, and the count of its glyphs. */
34
+ interface Expansion {
35
+ readonly advance: number;
36
+ readonly glyphs: number;
37
+ }
38
+
39
+ function larger(a: Expansion, b: Expansion): Expansion {
40
+ return { advance: Math.max(a.advance, b.advance), glyphs: Math.max(a.glyphs, b.glyphs) };
41
+ }
42
+
43
+ function sum(a: Expansion, b: Expansion): Expansion {
44
+ return { advance: a.advance + b.advance, glyphs: a.glyphs + b.glyphs };
45
+ }
46
+
47
+ /** This function tells if a rule can apply to the glyphs `reached`. A ligature needs its first glyph and all its components. */
48
+ function applies(rule: SubstitutionRule, reached: ReadonlySet<number>): boolean {
49
+ return reached.has(rule.from) && (rule.components ?? []).every((glyph) => reached.has(glyph));
50
+ }
51
+
52
+ /** This function adds the glyphs that the rules can give to `reached`. */
53
+ function reach(reached: Set<number>, rules: readonly SubstitutionRule[]): void {
54
+ for (let size = -1; size !== reached.size; ) {
55
+ size = reached.size;
56
+ for (const rule of rules) if (applies(rule, reached)) for (const glyph of rule.to) reached.add(glyph);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * This function applies one list of rules to the expansions. Each rule uses the values from before the list. A lookup applies
62
+ * once at a position, so its output does not go back into it. The function tells if a value changed.
63
+ */
64
+ function relax(rules: readonly SubstitutionRule[], expansions: Map<number, Expansion>): boolean {
65
+ const changes: (readonly [number, Expansion])[] = [];
66
+ for (const { from, to } of rules) {
67
+ const before = expansions.get(from)!;
68
+ const after = to.reduce((total, glyph) => sum(total, expansions.get(glyph)!), { advance: 0, glyphs: 0 });
69
+ if (after.advance > before.advance || after.glyphs > before.glyphs) changes.push([from, after]);
70
+ }
71
+ for (const [from, after] of changes) expansions.set(from, larger(expansions.get(from)!, after));
72
+ return changes.length > 0;
73
+ }
74
+
75
+ /**
76
+ * This function gives the largest expansion of each glyph of `reached`. Each ligature charges its first glyph, so each output
77
+ * glyph comes from one input glyph. Each pass goes backward over the lookups: a glyph then gets the expansions of the lookups
78
+ * after the lookup that makes it. A shaper with stages goes over the list again for each stage, so the passes repeat.
79
+ *
80
+ * One context rule can apply its nested lookups in any order. So the lists of a lookup with nested lookups repeat until no
81
+ * value changes, with the same limit. A value that stops changing holds for each order.
82
+ */
83
+ function expansionsOf(font: Font, reached: ReadonlySet<number>, lookups: readonly (readonly (readonly SubstitutionRule[])[])[], rounds: number): Map<number, Expansion> {
84
+ const expansions = new Map<number, Expansion>();
85
+ for (const glyph of reached) expansions.set(glyph, { advance: Math.max(0, font.advance(glyph)), glyphs: 1 });
86
+ const active = lookups.map((lists) => lists.map((rules) => rules.filter((rule) => applies(rule, reached))));
87
+ for (let round = 0; round < rounds; round++) {
88
+ let changed = false;
89
+ for (let i = active.length - 1; i >= 0; i--) {
90
+ const lists = active[i]!;
91
+ for (let step = 0; step < (lists.length > 1 ? ROUNDS : 1); step++) {
92
+ let moved = false;
93
+ for (const rules of lists) moved = relax(rules, expansions) || moved;
94
+ if (!moved) break;
95
+ changed = true;
96
+ }
97
+ }
98
+ if (!changed) break;
99
+ }
100
+ return expansions;
101
+ }
102
+
103
+ /**
104
+ * This function gives a limit for the width of each text over the characters `characters`, with no measurement. For each such
105
+ * text, `measureWidth(font, text, size, options)` is at most `width(text.length, size)` of the result. Give the same `options`.
106
+ * A text with another character has no limit from this function.
107
+ *
108
+ * The limit is sound, not tight. It follows the widest character of the set. It includes each glyph of the GSUB closure, each
109
+ * dotted circle, each part or composite of the Indic normalization, and the largest positive kerning.
110
+ */
111
+ export function widthBound(font: Font, characters: string, options: MeasureOptions = {}): WidthBound {
112
+ const { script, features, shaper, kerning: pairLookups } = shapingOf(font, options);
113
+ const set = new Set(Array.from(characters, (character) => character.codePointAt(0)!));
114
+ const indic = shaper === "indic" ? indicForms(set, (codePoint) => font.glyph(codePoint) !== 0) : undefined;
115
+
116
+ // The roots: the glyph of each code point that the shaper can see. A character with no glyph gives the glyph 0
117
+ const reached = new Set([...(indic?.codePoints ?? set)].map((codePoint) => font.glyph(codePoint)));
118
+ // A union of the features gives the same lookups as the union of the stages. Each stage then counts, in the rounds
119
+ const tags = shaper === "indic" ? indicFeatures(features) : shaper === "joining" ? joiningStages(features, true).flatMap((stage) => stage.features) : features;
120
+ const lookups = font.substitutions(script, tags).map((lookup) => lookup.rules());
121
+ reach(reached, lookups.flat(2));
122
+ // The plain shaper is one stage: it applies each lookup once, in the order of the list
123
+ const expansions = expansionsOf(font, reached, lookups, shaper === "plain" ? 1 : ROUNDS);
124
+
125
+ const form = (codePoint: number): Expansion => (indic?.composites.get(codePoint) ?? []).reduce((best, composite) => larger(best, form(composite)), expansions.get(font.glyph(codePoint))!);
126
+ // The Indic shaper can insert a dotted circle before each code point. Without a glyph, the code point U+25CC gives the glyph 0.
127
+ // One code point gets at most one: a circle of the vowel constraints takes the next matra, so that syllable is not broken
128
+ const inserted = indic === undefined ? { advance: 0, glyphs: 0 } : expansions.get(font.glyph(0x25cc))!;
129
+ let largest: Expansion = { advance: 0, glyphs: 0 };
130
+ for (const codePoint of set) {
131
+ const parts = indic?.parts.get(codePoint) ?? [codePoint];
132
+ largest = larger(largest, parts.reduce((total, part) => sum(total, sum(form(part), inserted)), { advance: 0, glyphs: 0 }));
133
+ }
134
+
135
+ // One glyph gets at most one first value and one second value from each lookup, and each subtable of a lookup is an alternative
136
+ let kerning = 0;
137
+ for (const lookup of pairLookups) {
138
+ const { first, second } = lookup.largest(reached);
139
+ kerning += first + second;
140
+ }
141
+
142
+ const { advance, glyphs } = largest;
143
+ const pixel = options.rounding === "pixel";
144
+ const float32 = options.sum === "float32" && !pixel;
145
+ // Pixel: an advance gets at most 1/128 px from the grid of 26.6, and then 0.5 px. A kerning sum gets at most 0.5 px
146
+ const pixelMargin = pixel ? 0.5 + 1 / 128 + (kerning > 0 ? 0.5 : 0) : 0;
147
+ // Float32: each lookup gives a glyph two positions, in units of 1/65536 px. A negative value gets no help from the relative
148
+ // factor below, so it has its own margin. The floor of a value and the round of a delta give 1 unit, the floor of the
149
+ // multiplier gives 0.5 units, and the cut of the scale gives KERNING_RANGE / unitsPerEm units to each of the two parts.
150
+ // The single precision gives FLOAT32 to the value and 4 FLOAT32 to the delta
151
+ const positions = pairLookups.length * 2;
152
+ const positionMargin = float32 ? (positions * (1.5 + (2 * KERNING_RANGE) / font.unitsPerEm)) / 65536 : 0;
153
+ const kerningMargin = float32 ? positions * 5 * FLOAT32 * KERNING_RANGE : 0;
154
+ return {
155
+ advance,
156
+ kerning,
157
+ glyphs,
158
+ width(length: number, size: number): number {
159
+ const total = length * ((advance + glyphs * (kerning + kerningMargin)) * (size / font.unitsPerEm) + glyphs * (pixelMargin + positionMargin));
160
+ // Float32: the fround of a glyph or 4 steps of a positive delta, the fround of a segment, and one step of the sum for
161
+ // each segment. A text has at most one segment for each UTF-16 unit
162
+ const single = float32 ? (1 + FLOAT32) ** (length + 6) : 1;
163
+ // The double precision: the scale and a few products, and one rounding for each step of a sum of fractions
164
+ return total * single * (1 + (2 ** 13 + length) * FLOAT64);
165
+ },
166
+ };
167
+ }
@@ -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
+ }