@weasel-js/text-shaping 0.0.0-stage → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -2
- package/dist/index.d.ts +56 -0
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/text-shaping
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenType shaping through [HarfBuzz](https://harfbuzz.github.io/): font bytes and text in,
|
|
4
|
+
positioned glyphs out. It is what lays out the scripts whose letters change with their
|
|
5
|
+
neighbors: Arabic joining, Indic conjuncts and vowel signs drawn before their consonant,
|
|
6
|
+
Thai and Khmer mark stacking, ligatures, and a face's own small capitals.
|
|
7
|
+
|
|
8
|
+
Nothing else in weasel depends on this package. A text layout handed a shaper uses it, and
|
|
9
|
+
one that isn't finds each character's glyph on its own and kerns pairs, which is right for
|
|
10
|
+
Latin, Greek, Cyrillic and CJK. Importing it costs nothing: HarfBuzz's WebAssembly (about
|
|
11
|
+
450 kB) loads when `loadShaper` is called.
|
|
12
|
+
|
|
13
|
+
## Giving it to weasel's text
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { loadShaper } from '@weasel-js/text-shaping';
|
|
17
|
+
import { bidi } from '@weasel-js/bidi';
|
|
18
|
+
import { registerBidi, registerShaper } from '@weasel-js/core';
|
|
19
|
+
|
|
20
|
+
registerBidi(bidi);
|
|
21
|
+
registerShaper(await loadShaper()); // once, at startup, before text is laid out
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Shaping reaches text set in a face registered with `registerFontOutlines` from TrueType or
|
|
25
|
+
OpenType bytes: its glyphs are drawn from the outlines by index, so any glyph a shaping
|
|
26
|
+
produces can be drawn. Text served by a baked or canvas atlas looks glyphs up by character
|
|
27
|
+
and is laid out as before; so is a face loaded from WOFF, whose bytes HarfBuzz cannot read.
|
|
28
|
+
Right-to-left text also needs the bidi engine, which puts its letters in visual order.
|
|
29
|
+
|
|
30
|
+
With a shaper registered, `fontVariantCaps: 'small-caps'` on a face that has an `smcp`
|
|
31
|
+
feature draws the face's small capitals rather than shrunken capitals, and `lang` (a BCP 47
|
|
32
|
+
tag, `'sr'`, `'ur'`) on a run or a node's style selects the face's forms for that language.
|
|
33
|
+
|
|
34
|
+
## Using it alone
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const shaper = await loadShaper();
|
|
38
|
+
for (const g of shaper.shape(fontBytes, 'بيت')) {
|
|
39
|
+
// g.gid, g.advance, g.dx, g.dy in ems; g.cluster indexes the text
|
|
40
|
+
const d = shaper.glyphD(fontBytes, g.gid, g.dx, g.dy);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
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
|
|
46
|
+
language tag. `glyphD` is em-space SVG path data, y
|
|
47
|
+
down. Pass the same `ArrayBuffer` for the same face every time: the parsed face is held
|
|
48
|
+
against it.
|
|
49
|
+
|
|
50
|
+
## Bundling
|
|
51
|
+
|
|
52
|
+
harfbuzzjs finds its `.wasm` beside its own module with `new URL('harfbuzz.wasm',
|
|
53
|
+
import.meta.url)`. Vite needs no configuration for it: a production build emits the file
|
|
54
|
+
as an asset and points the URL at it, and the dev server's dependency pre-bundle points the
|
|
55
|
+
URL back at the file in `node_modules`.
|
|
56
|
+
|
|
57
|
+
harfbuzzjs awaits at its module's top level, so the bundle must target ES2022 or later.
|
|
58
|
+
Loading it through `loadShaper`'s dynamic `import()` does not hide that from a bundler:
|
|
59
|
+
esbuild (and Vite 7 and earlier, which pre-bundle with it) refuses a lower target, while
|
|
60
|
+
Vite 8 warns and leaves the `await` in place.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** One glyph a shaping produced. Every length is in ems of the face, and y
|
|
2
|
+
* grows downward. */
|
|
3
|
+
interface ShapedGlyph {
|
|
4
|
+
/** The glyph's index in the face. 0 is `.notdef`: the face has no glyph
|
|
5
|
+
* for its cluster. */
|
|
6
|
+
gid: number;
|
|
7
|
+
/** UTF-16 offset in the shaped text where its cluster starts. A cluster is
|
|
8
|
+
* the smallest stretch of text and glyphs that map onto each other: a
|
|
9
|
+
* ligature's cluster holds every character it joined. */
|
|
10
|
+
cluster: number;
|
|
11
|
+
/** How far the pen moves past it. */
|
|
12
|
+
advance: number;
|
|
13
|
+
/** Where it is drawn, from the pen. */
|
|
14
|
+
dx: number;
|
|
15
|
+
dy: number;
|
|
16
|
+
/** Whether its piece of the text was shaped right to left, which also
|
|
17
|
+
* means HarfBuzz drew any mirrored character (a bracket) as its mirror. */
|
|
18
|
+
rtl: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** A shaping engine for weasel's text layout: what `registerShaper` takes. */
|
|
21
|
+
interface Shaper {
|
|
22
|
+
/**
|
|
23
|
+
* Shape `text` set in the face whose font file is `face` (an sfnt: TrueType
|
|
24
|
+
* or OpenType, not WOFF), with the OpenType `features` turned on.
|
|
25
|
+
*
|
|
26
|
+
* `language` is a BCP 47 tag (`'sr'`, `'ur'`) and selects the face's forms
|
|
27
|
+
* for that language: Serbian Cyrillic italics, Urdu digits. Absent, the
|
|
28
|
+
* face's default forms are drawn.
|
|
29
|
+
*
|
|
30
|
+
* Glyphs come in logical order: clusters by ascending `cluster`, and a
|
|
31
|
+
* cluster's own glyphs in the order they are drawn, left to right. The array
|
|
32
|
+
* is shared with later calls asking the same; never mutate it.
|
|
33
|
+
*/
|
|
34
|
+
shape(face: ArrayBuffer, text: string, features?: readonly string[], language?: string): readonly ShapedGlyph[];
|
|
35
|
+
/** Em-space SVG path data for glyph `gid` of `face`, moved by `(dx, dy)`
|
|
36
|
+
* ems; y grows downward and the origin is the pen on the baseline. Null for
|
|
37
|
+
* a glyph with no contours. */
|
|
38
|
+
glyphD(face: ArrayBuffer, gid: number, dx?: number, dy?: number): string | null;
|
|
39
|
+
/** Whether `face` has the OpenType feature `tag` (`'smcp'`, `'liga'`) in its
|
|
40
|
+
* substitutions or positioning. */
|
|
41
|
+
hasFeature(face: ArrayBuffer, tag: string): boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load HarfBuzz (about 450 kB of WebAssembly) and resolve with a shaper over
|
|
45
|
+
* it. Once per process: every call answers the same shaper.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { loadShaper } from '@weasel-js/text-shaping';
|
|
49
|
+
* import { registerShaper } from '@weasel-js/core';
|
|
50
|
+
*
|
|
51
|
+
* registerShaper(await loadShaper());
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function loadShaper(): Promise<Shaper>;
|
|
55
|
+
|
|
56
|
+
export { type ShapedGlyph, type Shaper, loadShaper };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// 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;
|
|
49
|
+
}
|
|
50
|
+
var RTL_BLOCKS = [
|
|
51
|
+
[1424, 2303],
|
|
52
|
+
[64285, 65023],
|
|
53
|
+
[65136, 65279],
|
|
54
|
+
[67584, 69631],
|
|
55
|
+
[124928, 126975]
|
|
56
|
+
];
|
|
57
|
+
function scriptPieces(text) {
|
|
58
|
+
const pieces = [];
|
|
59
|
+
let current = NONE;
|
|
60
|
+
let start = 0;
|
|
61
|
+
let rtl = false;
|
|
62
|
+
let at = 0;
|
|
63
|
+
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 });
|
|
68
|
+
start = at;
|
|
69
|
+
}
|
|
70
|
+
if (script !== current) rtl = isRtl(script, ch.codePointAt(0));
|
|
71
|
+
current = script;
|
|
72
|
+
}
|
|
73
|
+
at += ch.length;
|
|
74
|
+
}
|
|
75
|
+
if (at > start) pieces.push({ start, end: at, rtl });
|
|
76
|
+
return pieces;
|
|
77
|
+
}
|
|
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
|
+
|
|
83
|
+
// src/shaper.ts
|
|
84
|
+
var SHAPE_CACHE_LIMIT = 512;
|
|
85
|
+
var PRECISION = 1e5;
|
|
86
|
+
var fmt = (v) => String(Math.round(v * PRECISION) / PRECISION);
|
|
87
|
+
var HarfBuzzShaper = class {
|
|
88
|
+
#hb;
|
|
89
|
+
#faces = /* @__PURE__ */ new WeakMap();
|
|
90
|
+
#features = /* @__PURE__ */ new Map();
|
|
91
|
+
constructor(hb) {
|
|
92
|
+
this.#hb = hb;
|
|
93
|
+
}
|
|
94
|
+
#face(bytes) {
|
|
95
|
+
let rec = this.#faces.get(bytes);
|
|
96
|
+
if (rec) return rec;
|
|
97
|
+
const hb = this.#hb;
|
|
98
|
+
const face = new hb.Face(new hb.Blob(bytes));
|
|
99
|
+
rec = {
|
|
100
|
+
font: new hb.Font(face),
|
|
101
|
+
upem: face.upem,
|
|
102
|
+
features: /* @__PURE__ */ new Set([...face.getTableFeatureTags("GSUB"), ...face.getTableFeatureTags("GPOS")]),
|
|
103
|
+
paths: /* @__PURE__ */ new Map(),
|
|
104
|
+
shapes: /* @__PURE__ */ new Map()
|
|
105
|
+
};
|
|
106
|
+
this.#faces.set(bytes, rec);
|
|
107
|
+
return rec;
|
|
108
|
+
}
|
|
109
|
+
#feature(tag) {
|
|
110
|
+
let f = this.#features.get(tag);
|
|
111
|
+
if (!f) {
|
|
112
|
+
const parsed = this.#hb.Feature.fromString(tag);
|
|
113
|
+
if (!parsed) throw new Error(`weasel shape: "${tag}" is not an OpenType feature setting`);
|
|
114
|
+
f = parsed;
|
|
115
|
+
this.#features.set(tag, f);
|
|
116
|
+
}
|
|
117
|
+
return f;
|
|
118
|
+
}
|
|
119
|
+
shape(bytes, text, features = [], language) {
|
|
120
|
+
const rec = this.#face(bytes);
|
|
121
|
+
const lang = language || void 0;
|
|
122
|
+
const key = `${features.join(",")}\0${lang ?? ""}\0${text}`;
|
|
123
|
+
const held = rec.shapes.get(key);
|
|
124
|
+
if (held) return held;
|
|
125
|
+
const hbFeatures = features.map((tag) => this.#feature(tag));
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const piece of scriptPieces(text)) this.#piece(rec, text, piece, hbFeatures, lang, out);
|
|
128
|
+
if (rec.shapes.size >= SHAPE_CACHE_LIMIT) rec.shapes.clear();
|
|
129
|
+
rec.shapes.set(key, out);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Shape `piece` of `text`, the rest of `text` around it as context, and
|
|
133
|
+
* append its glyphs to `out` in logical order. */
|
|
134
|
+
#piece(rec, text, piece, features, lang, out) {
|
|
135
|
+
const hb = this.#hb;
|
|
136
|
+
const { start, end, rtl } = piece;
|
|
137
|
+
const buffer = new hb.Buffer();
|
|
138
|
+
buffer.addText(text, start, end - start);
|
|
139
|
+
buffer.setDirection(rtl ? hb.Direction.RTL : hb.Direction.LTR);
|
|
140
|
+
if (lang !== void 0) buffer.setLanguage(lang);
|
|
141
|
+
buffer.guessSegmentProperties();
|
|
142
|
+
buffer.setClusterLevel(hb.ClusterLevel.MONOTONE_CHARACTERS);
|
|
143
|
+
hb.shape(rec.font, buffer, features);
|
|
144
|
+
const infos = buffer.getGlyphInfos();
|
|
145
|
+
const positions = buffer.getGlyphPositions();
|
|
146
|
+
const em = rec.upem;
|
|
147
|
+
const glyphs = infos.map((info, i) => ({
|
|
148
|
+
gid: info.codepoint,
|
|
149
|
+
cluster: info.cluster,
|
|
150
|
+
advance: positions[i].xAdvance / em,
|
|
151
|
+
dx: positions[i].xOffset / em,
|
|
152
|
+
dy: -positions[i].yOffset / em,
|
|
153
|
+
rtl
|
|
154
|
+
}));
|
|
155
|
+
if (!rtl) {
|
|
156
|
+
for (const g of glyphs) out.push(g);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let hi = glyphs.length;
|
|
160
|
+
while (hi > 0) {
|
|
161
|
+
let lo = hi - 1;
|
|
162
|
+
while (lo > 0 && glyphs[lo - 1].cluster === glyphs[hi - 1].cluster) lo--;
|
|
163
|
+
for (let k = lo; k < hi; k++) out.push(glyphs[k]);
|
|
164
|
+
hi = lo;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
glyphD(bytes, gid, dx = 0, dy = 0) {
|
|
168
|
+
const rec = this.#face(bytes);
|
|
169
|
+
let cmds = rec.paths.get(gid);
|
|
170
|
+
if (cmds === void 0) {
|
|
171
|
+
cmds = rec.font.glyphToJson(gid);
|
|
172
|
+
rec.paths.set(gid, cmds);
|
|
173
|
+
}
|
|
174
|
+
if (cmds.length === 0) return null;
|
|
175
|
+
const em = rec.upem;
|
|
176
|
+
let d = "";
|
|
177
|
+
for (const { type, values } of cmds) {
|
|
178
|
+
d += type;
|
|
179
|
+
for (let k = 0; k < values.length; k += 2) {
|
|
180
|
+
d += `${k === 0 ? "" : " "}${fmt(values[k] / em + dx)} ${fmt(-values[k + 1] / em + dy)}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return d;
|
|
184
|
+
}
|
|
185
|
+
hasFeature(bytes, tag) {
|
|
186
|
+
return this.#face(bytes).features.has(tag);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var loading = null;
|
|
190
|
+
function loadShaper() {
|
|
191
|
+
loading ??= import('harfbuzzjs').then((hb) => new HarfBuzzShaper(hb));
|
|
192
|
+
return loading;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { loadShaper };
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
|
197
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/text-shaping",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.14.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"harfbuzzjs": "1.6.4"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@weasel-js/bidi": "1.14.0",
|
|
23
|
+
"@weasel-js/font": "1.14.0",
|
|
24
|
+
"@weasel-js/text": "1.14.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"build": "tsup"
|
|
29
|
+
},
|
|
30
|
+
"author": "orochi235",
|
|
31
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
35
|
+
"directory": "packages/text-shaping"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
}
|
|
52
|
+
}
|