@weasel-js/text-shaping 1.14.0 → 1.14.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 CHANGED
@@ -41,8 +41,9 @@ for (const g of shaper.shape(fontBytes, 'بيت')) {
41
41
  }
42
42
  ```
43
43
 
44
- `shape` cuts the text where its script changes and shapes each piece in that script's
45
- direction, then returns the glyphs in logical order. Its optional fourth argument is a BCP 47
44
+ `shape` cuts the text where its Unicode script changes, keeping spaces, digits, punctuation
45
+ and marks with the letters around them and a closing bracket with its opening one, and
46
+ shapes each piece in its letters' direction, then returns the glyphs in logical order. Its optional fourth argument is a BCP 47
46
47
  language tag. `glyphD` is em-space SVG path data, y
47
48
  down. Pass the same `ArrayBuffer` for the same face every time: the parsed face is held
48
49
  against it.
package/dist/index.js CHANGED
@@ -1,84 +1,89 @@
1
+ import { pairedBracket, bidiClassOf } from '@weasel-js/bidi';
2
+
1
3
  // src/scripts.ts
2
- var NAMES = [
3
- "Latin",
4
- "Greek",
5
- "Cyrillic",
6
- "Armenian",
7
- "Hebrew",
8
- "Arabic",
9
- "Syriac",
10
- "Thaana",
11
- "Nko",
12
- "Devanagari",
13
- "Bengali",
14
- "Gurmukhi",
15
- "Gujarati",
16
- "Oriya",
17
- "Tamil",
18
- "Telugu",
19
- "Kannada",
20
- "Malayalam",
21
- "Sinhala",
22
- "Thai",
23
- "Lao",
24
- "Tibetan",
25
- "Myanmar",
26
- "Georgian",
27
- "Hangul",
28
- "Ethiopic",
29
- "Khmer",
30
- "Mongolian",
31
- "Han",
32
- "Hiragana",
33
- "Katakana",
34
- "Adlam",
35
- "Javanese",
36
- "Balinese"
37
- ];
38
- var SCRIPTS = NAMES.map((name) => new RegExp(`^\\p{Script=${name}}$`, "u"));
39
- var RTL_NAMES = /* @__PURE__ */ new Set(["Hebrew", "Arabic", "Syriac", "Thaana", "Nko", "Adlam"]);
40
- var SHARED = /^[\p{Script=Common}\p{Script=Inherited}]$/u;
41
- var OTHER = SCRIPTS.length;
42
- var NONE = -1;
43
- function scriptOf(ch) {
44
- if (SHARED.test(ch)) return NONE;
45
- const cp = ch.codePointAt(0);
46
- if (cp < 592) return 0;
47
- for (let i = 0; i < SCRIPTS.length; i++) if (SCRIPTS[i].test(ch)) return i;
48
- return OTHER;
4
+ var SCRIPT_NAMES = `Latin Arabic Cyrillic Han Hiragana Katakana Hangul Devanagari Greek Hebrew Thai Bengali
5
+ Tamil Telugu Gujarati Kannada Malayalam Gurmukhi Oriya Sinhala Myanmar Khmer Lao Georgian Armenian
6
+ Ethiopic Tibetan Syriac Thaana Nko Mongolian Bopomofo Javanese Balinese Adlam Samaritan Mandaic
7
+ Cherokee Canadian_Aboriginal Ogham Runic Yi Old_Italic Gothic Deseret Tagalog Hanunoo Buhid Tagbanwa
8
+ Limbu Tai_Le Linear_B Ugaritic Shavian Osmanya Cypriot Braille Buginese Coptic New_Tai_Lue Glagolitic
9
+ Tifinagh Syloti_Nagri Old_Persian Kharoshthi Cuneiform Phoenician Phags_Pa Sundanese Lepcha Ol_Chiki
10
+ Vai Saurashtra Kayah_Li Rejang Lycian Carian Lydian Cham Tai_Tham Tai_Viet Avestan Egyptian_Hieroglyphs
11
+ Bamum Lisu Meetei_Mayek Imperial_Aramaic Old_South_Arabian Inscriptional_Parthian Inscriptional_Pahlavi
12
+ Old_Turkic Kaithi Batak Brahmi Chakma Meroitic_Cursive Meroitic_Hieroglyphs Miao Sharada Sora_Sompeng
13
+ Takri Caucasian_Albanian Bassa_Vah Duployan Elbasan Grantha Pahawh_Hmong Khojki Linear_A Mahajani
14
+ Manichaean Mende_Kikakui Modi Mro Old_North_Arabian Nabataean Palmyrene Pau_Cin_Hau Old_Permic
15
+ Psalter_Pahlavi Siddham Khudawadi Tirhuta Warang_Citi Ahom Anatolian_Hieroglyphs Hatran Multani
16
+ Old_Hungarian SignWriting Bhaiksuki Marchen Newa Osage Tangut Masaram_Gondi Nushu Soyombo
17
+ Zanabazar_Square Dogra Gunjala_Gondi Makasar Medefaidrin Hanifi_Rohingya Sogdian Old_Sogdian Elymaic
18
+ Nandinagari Nyiakeng_Puachue_Hmong Wancho Chorasmian Dives_Akuru Khitan_Small_Script Yezidi
19
+ Cypro_Minoan Old_Uyghur Tangsa Toto Vithkuqi Kawi Nag_Mundari Garay Gurung_Khema Kirat_Rai Ol_Onal
20
+ Sunuwar Todhri Tulu_Tigalari Sidetic Tai_Yo Beria_Erfe Tolong_Siki`.split(/\s+/);
21
+ var SHARED = /^[\p{Script=Common}\p{Script=Inherited}\p{Script=Unknown}]$/u;
22
+ var tests = null;
23
+ function scriptTests() {
24
+ if (tests) return tests;
25
+ tests = /* @__PURE__ */ new Map();
26
+ for (const name of SCRIPT_NAMES) {
27
+ try {
28
+ tests.set(name, new RegExp(`^\\p{Script=${name}}$`, "u"));
29
+ } catch {
30
+ }
31
+ }
32
+ return tests;
33
+ }
34
+ function scriptOf(ch, current) {
35
+ if (SHARED.test(ch)) return null;
36
+ if (ch.codePointAt(0) < 592) return "Latin";
37
+ const all = scriptTests();
38
+ if (current !== null && all.get(current)?.test(ch)) return current;
39
+ for (const [name, test] of all) if (test.test(ch)) return name;
40
+ return null;
49
41
  }
50
- var RTL_BLOCKS = [
51
- [1424, 2303],
52
- [64285, 65023],
53
- [65136, 65279],
54
- [67584, 69631],
55
- [124928, 126975]
56
- ];
42
+ var MAX_OPEN = 64;
57
43
  function scriptPieces(text) {
58
44
  const pieces = [];
59
- let current = NONE;
45
+ const open = [];
46
+ let current = null;
47
+ let rtl = null;
60
48
  let start = 0;
61
- let rtl = false;
62
49
  let at = 0;
63
50
  for (const ch of text) {
64
- const script = scriptOf(ch);
65
- if (script !== NONE) {
66
- if (current !== NONE && script !== current) {
67
- pieces.push({ start, end: at, rtl });
51
+ const cp = ch.codePointAt(0);
52
+ let script = scriptOf(ch, current);
53
+ if (script === null) {
54
+ const bracket = pairedBracket(cp);
55
+ if (bracket?.kind === "open") {
56
+ if (open.length === MAX_OPEN) open.shift();
57
+ open.push({ close: bracket.pair, script: current });
58
+ } else if (bracket?.kind === "close") {
59
+ let k = open.length - 1;
60
+ while (k >= 0 && open[k].close !== cp) k--;
61
+ if (k >= 0) {
62
+ script = open[k].script;
63
+ open.length = k;
64
+ }
65
+ }
66
+ }
67
+ if (script !== null) {
68
+ if (current === null) {
69
+ for (const o of open) o.script ??= script;
70
+ } else if (script !== current) {
71
+ pieces.push({ start, end: at, rtl: rtl ?? false });
68
72
  start = at;
73
+ rtl = null;
69
74
  }
70
- if (script !== current) rtl = isRtl(script, ch.codePointAt(0));
71
75
  current = script;
76
+ if (rtl === null) {
77
+ const cls = bidiClassOf(cp);
78
+ if (cls === "L") rtl = false;
79
+ else if (cls === "R" || cls === "AL") rtl = true;
80
+ }
72
81
  }
73
82
  at += ch.length;
74
83
  }
75
- if (at > start) pieces.push({ start, end: at, rtl });
84
+ if (at > start) pieces.push({ start, end: at, rtl: rtl ?? false });
76
85
  return pieces;
77
86
  }
78
- function isRtl(script, cp) {
79
- if (script !== OTHER) return RTL_NAMES.has(NAMES[script]);
80
- return RTL_BLOCKS.some(([lo, hi]) => cp >= lo && cp <= hi);
81
- }
82
87
 
83
88
  // src/shaper.ts
84
89
  var SHAPE_CACHE_LIMIT = 512;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/scripts.ts","../src/shaper.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Cutting text where its script changes. HarfBuzz shapes a buffer as one\n * script in one direction, so a run mixing Latin and Arabic is shaped a piece\n * at a time.\n */\n\n/** The scripts a run is cut between. Any other script is one more piece,\n * shaped as whichever script HarfBuzz guesses from its first letter. */\nconst NAMES = [\n 'Latin', 'Greek', 'Cyrillic', 'Armenian', 'Hebrew', 'Arabic', 'Syriac', 'Thaana', 'Nko',\n 'Devanagari', 'Bengali', 'Gurmukhi', 'Gujarati', 'Oriya', 'Tamil', 'Telugu', 'Kannada',\n 'Malayalam', 'Sinhala', 'Thai', 'Lao', 'Tibetan', 'Myanmar', 'Georgian', 'Hangul', 'Ethiopic',\n 'Khmer', 'Mongolian', 'Han', 'Hiragana', 'Katakana', 'Adlam', 'Javanese', 'Balinese',\n];\nconst SCRIPTS: readonly RegExp[] = NAMES.map((name) => new RegExp(`^\\\\p{Script=${name}}$`, 'u'));\n\n/** Which of them read right to left. */\nconst RTL_NAMES = new Set(['Hebrew', 'Arabic', 'Syriac', 'Thaana', 'Nko', 'Adlam']);\n\n/** Characters every script shares, which join the piece around them. */\nconst SHARED = /^[\\p{Script=Common}\\p{Script=Inherited}]$/u;\n\nconst OTHER = SCRIPTS.length;\nconst NONE = -1;\n\nfunction scriptOf(ch: string): number {\n if (SHARED.test(ch)) return NONE;\n const cp = ch.codePointAt(0)!;\n // Most text is Latin, and the Latin letters below U+0250 are all it holds there.\n if (cp < 0x250) return 0;\n for (let i = 0; i < SCRIPTS.length; i++) if (SCRIPTS[i].test(ch)) return i;\n return OTHER;\n}\n\n/** One stretch of text in a single script. */\nexport interface ScriptPiece {\n start: number;\n end: number;\n /** Whether the script reads right to left, which HarfBuzz shapes it in. */\n rtl: boolean;\n}\n\n/** Blocks whose scripts read right to left, for a script not named above. */\nconst RTL_BLOCKS: ReadonlyArray<readonly [number, number]> = [\n [0x0590, 0x08ff], [0xfb1d, 0xfdff], [0xfe70, 0xfeff], [0x10800, 0x10fff], [0x1e800, 0x1efff],\n];\n\n/**\n * `text` cut where its script changes, as UTF-16 spans covering all of it. A\n * character every script shares stays in the piece before it, or the one\n * after it when it opens the text. Empty text has no pieces.\n */\nexport function scriptPieces(text: string): ScriptPiece[] {\n const pieces: ScriptPiece[] = [];\n let current = NONE;\n let start = 0;\n let rtl = false;\n let at = 0;\n for (const ch of text) {\n const script = scriptOf(ch);\n if (script !== NONE) {\n if (current !== NONE && script !== current) {\n pieces.push({ start, end: at, rtl });\n start = at;\n }\n if (script !== current) rtl = isRtl(script, ch.codePointAt(0)!);\n current = script;\n }\n at += ch.length;\n }\n if (at > start) pieces.push({ start, end: at, rtl });\n return pieces;\n}\n\nfunction isRtl(script: number, cp: number): boolean {\n if (script !== OTHER) return RTL_NAMES.has(NAMES[script]);\n return RTL_BLOCKS.some(([lo, hi]) => cp >= lo && cp <= hi);\n}\n","/**\n * HarfBuzz, behind the shaping contract `@weasel-js/text` declares: font\n * bytes and text in, glyph indices with em-space advances and offsets out,\n * and the outline of any glyph a shaping produced.\n *\n * harfbuzzjs instantiates its WebAssembly when its module is evaluated, so it\n * is imported only by {@link loadShaper}: importing this package costs nothing\n * until a consumer asks for shaping.\n */\nimport { scriptPieces, type ScriptPiece } from './scripts';\n\ntype HarfBuzz = typeof import('harfbuzzjs');\ntype HbFont = InstanceType<HarfBuzz['Font']>;\ntype HbFeature = InstanceType<HarfBuzz['Feature']>;\n\n/** One glyph a shaping produced. Every length is in ems of the face, and y\n * grows downward. */\nexport interface ShapedGlyph {\n /** The glyph's index in the face. 0 is `.notdef`: the face has no glyph\n * for its cluster. */\n gid: number;\n /** UTF-16 offset in the shaped text where its cluster starts. A cluster is\n * the smallest stretch of text and glyphs that map onto each other: a\n * ligature's cluster holds every character it joined. */\n cluster: number;\n /** How far the pen moves past it. */\n advance: number;\n /** Where it is drawn, from the pen. */\n dx: number;\n dy: number;\n /** Whether its piece of the text was shaped right to left, which also\n * means HarfBuzz drew any mirrored character (a bracket) as its mirror. */\n rtl: boolean;\n}\n\n/** A shaping engine for weasel's text layout: what `registerShaper` takes. */\nexport interface Shaper {\n /**\n * Shape `text` set in the face whose font file is `face` (an sfnt: TrueType\n * or OpenType, not WOFF), with the OpenType `features` turned on.\n *\n * `language` is a BCP 47 tag (`'sr'`, `'ur'`) and selects the face's forms\n * for that language: Serbian Cyrillic italics, Urdu digits. Absent, the\n * face's default forms are drawn.\n *\n * Glyphs come in logical order: clusters by ascending `cluster`, and a\n * cluster's own glyphs in the order they are drawn, left to right. The array\n * is shared with later calls asking the same; never mutate it.\n */\n shape(face: ArrayBuffer, text: string, features?: readonly string[], language?: string): readonly ShapedGlyph[];\n /** Em-space SVG path data for glyph `gid` of `face`, moved by `(dx, dy)`\n * ems; y grows downward and the origin is the pen on the baseline. Null for\n * a glyph with no contours. */\n glyphD(face: ArrayBuffer, gid: number, dx?: number, dy?: number): string | null;\n /** Whether `face` has the OpenType feature `tag` (`'smcp'`, `'liga'`) in its\n * substitutions or positioning. */\n hasFeature(face: ArrayBuffer, tag: string): boolean;\n}\n\n/** Path commands in font units, y up, as HarfBuzz draws them. */\ninterface Command { type: string; values: number[] }\n\ninterface FaceRecord {\n font: HbFont;\n upem: number;\n features: ReadonlySet<string>;\n paths: Map<number, readonly Command[]>;\n shapes: Map<string, readonly ShapedGlyph[]>;\n}\n\n/** Shapings held per face before they are dropped together. */\nconst SHAPE_CACHE_LIMIT = 512;\n\n/** Decimal places kept in path data: 1e-5 em, and never exponent notation,\n * which an SVG path tokenizer would read as a command. */\nconst PRECISION = 1e5;\n\nconst fmt = (v: number): string => String(Math.round(v * PRECISION) / PRECISION);\n\nclass HarfBuzzShaper implements Shaper {\n readonly #hb: HarfBuzz;\n readonly #faces = new WeakMap<ArrayBuffer, FaceRecord>();\n readonly #features = new Map<string, HbFeature>();\n\n constructor(hb: HarfBuzz) {\n this.#hb = hb;\n }\n\n #face(bytes: ArrayBuffer): FaceRecord {\n let rec = this.#faces.get(bytes);\n if (rec) return rec;\n const hb = this.#hb;\n const face = new hb.Face(new hb.Blob(bytes));\n rec = {\n font: new hb.Font(face),\n upem: face.upem,\n features: new Set([...face.getTableFeatureTags('GSUB'), ...face.getTableFeatureTags('GPOS')]),\n paths: new Map(),\n shapes: new Map(),\n };\n this.#faces.set(bytes, rec);\n return rec;\n }\n\n #feature(tag: string): HbFeature {\n let f = this.#features.get(tag);\n if (!f) {\n const parsed = this.#hb.Feature.fromString(tag);\n if (!parsed) throw new Error(`weasel shape: \"${tag}\" is not an OpenType feature setting`);\n f = parsed;\n this.#features.set(tag, f);\n }\n return f;\n }\n\n shape(\n bytes: ArrayBuffer, text: string, features: readonly string[] = [], language?: string,\n ): readonly ShapedGlyph[] {\n const rec = this.#face(bytes);\n const lang = language || undefined;\n const key = `${features.join(',')}\\u0000${lang ?? ''}\\u0000${text}`;\n const held = rec.shapes.get(key);\n if (held) return held;\n const hbFeatures = features.map((tag) => this.#feature(tag));\n const out: ShapedGlyph[] = [];\n for (const piece of scriptPieces(text)) this.#piece(rec, text, piece, hbFeatures, lang, out);\n if (rec.shapes.size >= SHAPE_CACHE_LIMIT) rec.shapes.clear();\n rec.shapes.set(key, out);\n return out;\n }\n\n /** Shape `piece` of `text`, the rest of `text` around it as context, and\n * append its glyphs to `out` in logical order. */\n #piece(\n rec: FaceRecord, text: string, piece: ScriptPiece, features: HbFeature[], lang: string | undefined,\n out: ShapedGlyph[],\n ): void {\n const hb = this.#hb;\n const { start, end, rtl } = piece;\n const buffer = new hb.Buffer();\n buffer.addText(text, start, end - start);\n buffer.setDirection(rtl ? hb.Direction.RTL : hb.Direction.LTR);\n if (lang !== undefined) buffer.setLanguage(lang);\n // Fills in the script, and the language when none was set; what was set stands.\n buffer.guessSegmentProperties();\n // Marks keep clusters of their own, so each lands in the slot of the\n // character it was written as rather than merging into its base's.\n buffer.setClusterLevel(hb.ClusterLevel.MONOTONE_CHARACTERS);\n hb.shape(rec.font, buffer, features);\n const infos = buffer.getGlyphInfos();\n const positions = buffer.getGlyphPositions();\n const em = rec.upem;\n const glyphs: ShapedGlyph[] = infos.map((info, i) => ({\n gid: info.codepoint,\n cluster: info.cluster,\n advance: positions[i].xAdvance / em,\n dx: positions[i].xOffset / em,\n dy: -positions[i].yOffset / em,\n rtl,\n }));\n if (!rtl) {\n for (const g of glyphs) out.push(g);\n return;\n }\n // Right to left, HarfBuzz hands the glyphs over in visual order. Reverse\n // the clusters into logical order and keep each one's own glyphs as drawn.\n let hi = glyphs.length;\n while (hi > 0) {\n let lo = hi - 1;\n while (lo > 0 && glyphs[lo - 1].cluster === glyphs[hi - 1].cluster) lo--;\n for (let k = lo; k < hi; k++) out.push(glyphs[k]);\n hi = lo;\n }\n }\n\n glyphD(bytes: ArrayBuffer, gid: number, dx = 0, dy = 0): string | null {\n const rec = this.#face(bytes);\n let cmds = rec.paths.get(gid);\n if (cmds === undefined) {\n cmds = rec.font.glyphToJson(gid) as readonly Command[];\n rec.paths.set(gid, cmds);\n }\n if (cmds.length === 0) return null;\n const em = rec.upem;\n let d = '';\n for (const { type, values } of cmds) {\n d += type;\n for (let k = 0; k < values.length; k += 2) {\n d += `${k === 0 ? '' : ' '}${fmt(values[k] / em + dx)} ${fmt(-values[k + 1] / em + dy)}`;\n }\n }\n return d;\n }\n\n hasFeature(bytes: ArrayBuffer, tag: string): boolean {\n return this.#face(bytes).features.has(tag);\n }\n}\n\nlet loading: Promise<Shaper> | null = null;\n\n/**\n * Load HarfBuzz (about 450 kB of WebAssembly) and resolve with a shaper over\n * it. Once per process: every call answers the same shaper.\n *\n * ```ts\n * import { loadShaper } from '@weasel-js/text-shaping';\n * import { registerShaper } from '@weasel-js/core';\n *\n * registerShaper(await loadShaper());\n * ```\n */\nexport function loadShaper(): Promise<Shaper> {\n loading ??= import('harfbuzzjs').then((hb) => new HarfBuzzShaper(hb));\n return loading;\n}\n"]}
1
+ {"version":3,"sources":["../src/scripts.ts","../src/shaper.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Cutting text where its script changes. HarfBuzz shapes a buffer as one\n * script in one direction, so a run mixing Latin and Arabic is shaped a piece\n * at a time.\n */\nimport { bidiClassOf, pairedBracket } from '@weasel-js/bidi';\n\n/**\n * Every value of the Unicode Script property through Unicode 17, but the three\n * that join their neighbors (Common, Inherited, Unknown). The runtime's\n * `\\p{Script=…}` answers which one a character is in; a name it does not know\n * yet is skipped, and its characters join their neighbors too.\n */\nexport const SCRIPT_NAMES = `Latin Arabic Cyrillic Han Hiragana Katakana Hangul Devanagari Greek Hebrew Thai Bengali\nTamil Telugu Gujarati Kannada Malayalam Gurmukhi Oriya Sinhala Myanmar Khmer Lao Georgian Armenian\nEthiopic Tibetan Syriac Thaana Nko Mongolian Bopomofo Javanese Balinese Adlam Samaritan Mandaic\nCherokee Canadian_Aboriginal Ogham Runic Yi Old_Italic Gothic Deseret Tagalog Hanunoo Buhid Tagbanwa\nLimbu Tai_Le Linear_B Ugaritic Shavian Osmanya Cypriot Braille Buginese Coptic New_Tai_Lue Glagolitic\nTifinagh Syloti_Nagri Old_Persian Kharoshthi Cuneiform Phoenician Phags_Pa Sundanese Lepcha Ol_Chiki\nVai Saurashtra Kayah_Li Rejang Lycian Carian Lydian Cham Tai_Tham Tai_Viet Avestan Egyptian_Hieroglyphs\nBamum Lisu Meetei_Mayek Imperial_Aramaic Old_South_Arabian Inscriptional_Parthian Inscriptional_Pahlavi\nOld_Turkic Kaithi Batak Brahmi Chakma Meroitic_Cursive Meroitic_Hieroglyphs Miao Sharada Sora_Sompeng\nTakri Caucasian_Albanian Bassa_Vah Duployan Elbasan Grantha Pahawh_Hmong Khojki Linear_A Mahajani\nManichaean Mende_Kikakui Modi Mro Old_North_Arabian Nabataean Palmyrene Pau_Cin_Hau Old_Permic\nPsalter_Pahlavi Siddham Khudawadi Tirhuta Warang_Citi Ahom Anatolian_Hieroglyphs Hatran Multani\nOld_Hungarian SignWriting Bhaiksuki Marchen Newa Osage Tangut Masaram_Gondi Nushu Soyombo\nZanabazar_Square Dogra Gunjala_Gondi Makasar Medefaidrin Hanifi_Rohingya Sogdian Old_Sogdian Elymaic\nNandinagari Nyiakeng_Puachue_Hmong Wancho Chorasmian Dives_Akuru Khitan_Small_Script Yezidi\nCypro_Minoan Old_Uyghur Tangsa Toto Vithkuqi Kawi Nag_Mundari Garay Gurung_Khema Kirat_Rai Ol_Onal\nSunuwar Todhri Tulu_Tigalari Sidetic Tai_Yo Beria_Erfe Tolong_Siki`.split(/\\s+/);\n\n/** Characters that take the script of the text around them. */\nconst SHARED = /^[\\p{Script=Common}\\p{Script=Inherited}\\p{Script=Unknown}]$/u;\n\nlet tests: Map<string, RegExp> | null = null;\n\n/** The script names this runtime knows, each with its test, in `SCRIPT_NAMES`' order. */\nfunction scriptTests(): Map<string, RegExp> {\n if (tests) return tests;\n tests = new Map();\n for (const name of SCRIPT_NAMES) {\n try {\n tests.set(name, new RegExp(`^\\\\p{Script=${name}}$`, 'u'));\n } catch {\n // A script newer than this runtime's Unicode.\n }\n }\n return tests;\n}\n\n/** The script `ch` is written in, or null when it takes its neighbors'.\n * `current` is tried first, since most characters continue the piece. */\nfunction scriptOf(ch: string, current: string | null): string | null {\n if (SHARED.test(ch)) return null;\n // Most text is Latin, and the letters below U+0250 that are not shared are all Latin.\n if (ch.codePointAt(0)! < 0x250) return 'Latin';\n const all = scriptTests();\n if (current !== null && all.get(current)?.test(ch)) return current;\n for (const [name, test] of all) if (test.test(ch)) return name;\n return null;\n}\n\n/** One stretch of text in a single script. */\nexport interface ScriptPiece {\n start: number;\n end: number;\n /** Whether the script reads right to left, which HarfBuzz shapes it in. */\n rtl: boolean;\n}\n\n/** An open bracket still waiting for its close, and the script it opened in. */\ninterface Open { close: number; script: string | null }\n\n/** Open brackets remembered at once; deeper nesting forgets the outermost. */\nconst MAX_OPEN = 64;\n\n/**\n * `text` cut where its script changes, as UTF-16 spans covering all of it.\n *\n * Following UAX #24, a character every script shares (space, digits,\n * punctuation, a combining mark) stays in the piece before it, or the one after\n * it when it opens the text; a closing bracket takes the script its opening\n * bracket was in, so the `)` of `abc (שלום) def` goes with the Latin. A\n * piece reads right to left when the first of its own letters with a strong\n * `Bidi_Class` is R or AL. Empty text has no pieces.\n */\nexport function scriptPieces(text: string): ScriptPiece[] {\n const pieces: ScriptPiece[] = [];\n const open: Open[] = [];\n let current: string | null = null;\n let rtl: boolean | null = null;\n let start = 0;\n let at = 0;\n for (const ch of text) {\n const cp = ch.codePointAt(0)!;\n let script = scriptOf(ch, current);\n if (script === null) {\n const bracket = pairedBracket(cp);\n if (bracket?.kind === 'open') {\n if (open.length === MAX_OPEN) open.shift();\n open.push({ close: bracket.pair, script: current });\n } else if (bracket?.kind === 'close') {\n let k = open.length - 1;\n while (k >= 0 && open[k].close !== cp) k--;\n if (k >= 0) {\n script = open[k].script;\n open.length = k;\n }\n }\n }\n if (script !== null) {\n if (current === null) {\n for (const o of open) o.script ??= script;\n } else if (script !== current) {\n pieces.push({ start, end: at, rtl: rtl ?? false });\n start = at;\n rtl = null;\n }\n current = script;\n if (rtl === null) {\n const cls = bidiClassOf(cp);\n if (cls === 'L') rtl = false;\n else if (cls === 'R' || cls === 'AL') rtl = true;\n }\n }\n at += ch.length;\n }\n if (at > start) pieces.push({ start, end: at, rtl: rtl ?? false });\n return pieces;\n}\n","/**\n * HarfBuzz, behind the shaping contract `@weasel-js/text` declares: font\n * bytes and text in, glyph indices with em-space advances and offsets out,\n * and the outline of any glyph a shaping produced.\n *\n * harfbuzzjs instantiates its WebAssembly when its module is evaluated, so it\n * is imported only by {@link loadShaper}: importing this package costs nothing\n * until a consumer asks for shaping.\n */\nimport { scriptPieces, type ScriptPiece } from './scripts';\n\ntype HarfBuzz = typeof import('harfbuzzjs');\ntype HbFont = InstanceType<HarfBuzz['Font']>;\ntype HbFeature = InstanceType<HarfBuzz['Feature']>;\n\n/** One glyph a shaping produced. Every length is in ems of the face, and y\n * grows downward. */\nexport interface ShapedGlyph {\n /** The glyph's index in the face. 0 is `.notdef`: the face has no glyph\n * for its cluster. */\n gid: number;\n /** UTF-16 offset in the shaped text where its cluster starts. A cluster is\n * the smallest stretch of text and glyphs that map onto each other: a\n * ligature's cluster holds every character it joined. */\n cluster: number;\n /** How far the pen moves past it. */\n advance: number;\n /** Where it is drawn, from the pen. */\n dx: number;\n dy: number;\n /** Whether its piece of the text was shaped right to left, which also\n * means HarfBuzz drew any mirrored character (a bracket) as its mirror. */\n rtl: boolean;\n}\n\n/** A shaping engine for weasel's text layout: what `registerShaper` takes. */\nexport interface Shaper {\n /**\n * Shape `text` set in the face whose font file is `face` (an sfnt: TrueType\n * or OpenType, not WOFF), with the OpenType `features` turned on.\n *\n * `language` is a BCP 47 tag (`'sr'`, `'ur'`) and selects the face's forms\n * for that language: Serbian Cyrillic italics, Urdu digits. Absent, the\n * face's default forms are drawn.\n *\n * Glyphs come in logical order: clusters by ascending `cluster`, and a\n * cluster's own glyphs in the order they are drawn, left to right. The array\n * is shared with later calls asking the same; never mutate it.\n */\n shape(face: ArrayBuffer, text: string, features?: readonly string[], language?: string): readonly ShapedGlyph[];\n /** Em-space SVG path data for glyph `gid` of `face`, moved by `(dx, dy)`\n * ems; y grows downward and the origin is the pen on the baseline. Null for\n * a glyph with no contours. */\n glyphD(face: ArrayBuffer, gid: number, dx?: number, dy?: number): string | null;\n /** Whether `face` has the OpenType feature `tag` (`'smcp'`, `'liga'`) in its\n * substitutions or positioning. */\n hasFeature(face: ArrayBuffer, tag: string): boolean;\n}\n\n/** Path commands in font units, y up, as HarfBuzz draws them. */\ninterface Command { type: string; values: number[] }\n\ninterface FaceRecord {\n font: HbFont;\n upem: number;\n features: ReadonlySet<string>;\n paths: Map<number, readonly Command[]>;\n shapes: Map<string, readonly ShapedGlyph[]>;\n}\n\n/** Shapings held per face before they are dropped together. */\nconst SHAPE_CACHE_LIMIT = 512;\n\n/** Decimal places kept in path data: 1e-5 em, and never exponent notation,\n * which an SVG path tokenizer would read as a command. */\nconst PRECISION = 1e5;\n\nconst fmt = (v: number): string => String(Math.round(v * PRECISION) / PRECISION);\n\nclass HarfBuzzShaper implements Shaper {\n readonly #hb: HarfBuzz;\n readonly #faces = new WeakMap<ArrayBuffer, FaceRecord>();\n readonly #features = new Map<string, HbFeature>();\n\n constructor(hb: HarfBuzz) {\n this.#hb = hb;\n }\n\n #face(bytes: ArrayBuffer): FaceRecord {\n let rec = this.#faces.get(bytes);\n if (rec) return rec;\n const hb = this.#hb;\n const face = new hb.Face(new hb.Blob(bytes));\n rec = {\n font: new hb.Font(face),\n upem: face.upem,\n features: new Set([...face.getTableFeatureTags('GSUB'), ...face.getTableFeatureTags('GPOS')]),\n paths: new Map(),\n shapes: new Map(),\n };\n this.#faces.set(bytes, rec);\n return rec;\n }\n\n #feature(tag: string): HbFeature {\n let f = this.#features.get(tag);\n if (!f) {\n const parsed = this.#hb.Feature.fromString(tag);\n if (!parsed) throw new Error(`weasel shape: \"${tag}\" is not an OpenType feature setting`);\n f = parsed;\n this.#features.set(tag, f);\n }\n return f;\n }\n\n shape(\n bytes: ArrayBuffer, text: string, features: readonly string[] = [], language?: string,\n ): readonly ShapedGlyph[] {\n const rec = this.#face(bytes);\n const lang = language || undefined;\n const key = `${features.join(',')}\\u0000${lang ?? ''}\\u0000${text}`;\n const held = rec.shapes.get(key);\n if (held) return held;\n const hbFeatures = features.map((tag) => this.#feature(tag));\n const out: ShapedGlyph[] = [];\n for (const piece of scriptPieces(text)) this.#piece(rec, text, piece, hbFeatures, lang, out);\n if (rec.shapes.size >= SHAPE_CACHE_LIMIT) rec.shapes.clear();\n rec.shapes.set(key, out);\n return out;\n }\n\n /** Shape `piece` of `text`, the rest of `text` around it as context, and\n * append its glyphs to `out` in logical order. */\n #piece(\n rec: FaceRecord, text: string, piece: ScriptPiece, features: HbFeature[], lang: string | undefined,\n out: ShapedGlyph[],\n ): void {\n const hb = this.#hb;\n const { start, end, rtl } = piece;\n const buffer = new hb.Buffer();\n buffer.addText(text, start, end - start);\n buffer.setDirection(rtl ? hb.Direction.RTL : hb.Direction.LTR);\n if (lang !== undefined) buffer.setLanguage(lang);\n // Fills in the script, and the language when none was set; what was set stands.\n buffer.guessSegmentProperties();\n // Marks keep clusters of their own, so each lands in the slot of the\n // character it was written as rather than merging into its base's.\n buffer.setClusterLevel(hb.ClusterLevel.MONOTONE_CHARACTERS);\n hb.shape(rec.font, buffer, features);\n const infos = buffer.getGlyphInfos();\n const positions = buffer.getGlyphPositions();\n const em = rec.upem;\n const glyphs: ShapedGlyph[] = infos.map((info, i) => ({\n gid: info.codepoint,\n cluster: info.cluster,\n advance: positions[i].xAdvance / em,\n dx: positions[i].xOffset / em,\n dy: -positions[i].yOffset / em,\n rtl,\n }));\n if (!rtl) {\n for (const g of glyphs) out.push(g);\n return;\n }\n // Right to left, HarfBuzz hands the glyphs over in visual order. Reverse\n // the clusters into logical order and keep each one's own glyphs as drawn.\n let hi = glyphs.length;\n while (hi > 0) {\n let lo = hi - 1;\n while (lo > 0 && glyphs[lo - 1].cluster === glyphs[hi - 1].cluster) lo--;\n for (let k = lo; k < hi; k++) out.push(glyphs[k]);\n hi = lo;\n }\n }\n\n glyphD(bytes: ArrayBuffer, gid: number, dx = 0, dy = 0): string | null {\n const rec = this.#face(bytes);\n let cmds = rec.paths.get(gid);\n if (cmds === undefined) {\n cmds = rec.font.glyphToJson(gid) as readonly Command[];\n rec.paths.set(gid, cmds);\n }\n if (cmds.length === 0) return null;\n const em = rec.upem;\n let d = '';\n for (const { type, values } of cmds) {\n d += type;\n for (let k = 0; k < values.length; k += 2) {\n d += `${k === 0 ? '' : ' '}${fmt(values[k] / em + dx)} ${fmt(-values[k + 1] / em + dy)}`;\n }\n }\n return d;\n }\n\n hasFeature(bytes: ArrayBuffer, tag: string): boolean {\n return this.#face(bytes).features.has(tag);\n }\n}\n\nlet loading: Promise<Shaper> | null = null;\n\n/**\n * Load HarfBuzz (about 450 kB of WebAssembly) and resolve with a shaper over\n * it. Once per process: every call answers the same shaper.\n *\n * ```ts\n * import { loadShaper } from '@weasel-js/text-shaping';\n * import { registerShaper } from '@weasel-js/core';\n *\n * registerShaper(await loadShaper());\n * ```\n */\nexport function loadShaper(): Promise<Shaper> {\n loading ??= import('harfbuzzjs').then((hb) => new HarfBuzzShaper(hb));\n return loading;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/text-shaping",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "OpenType text shaping for weasel through HarfBuzz: contextual forms, ligatures, reordering and mark placement for Arabic, the Indic scripts, Thai and the rest. Font bytes and text in, positioned glyphs out; no renderer and no scene graph.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,12 +16,12 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "dependencies": {
19
+ "@weasel-js/bidi": "1.14.2",
19
20
  "harfbuzzjs": "1.6.4"
20
21
  },
21
22
  "devDependencies": {
22
- "@weasel-js/bidi": "1.14.0",
23
- "@weasel-js/font": "1.14.0",
24
- "@weasel-js/text": "1.14.0"
23
+ "@weasel-js/font": "1.14.2",
24
+ "@weasel-js/text": "1.14.2"
25
25
  },
26
26
  "scripts": {
27
27
  "test": "vitest run",