@unisim/doc 0.6.0 → 0.8.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/README.md CHANGED
@@ -50,6 +50,17 @@ reader has to drop is reported through `RichDoc.notices` rather than discovered
50
50
  later — the failure worth designing against is someone assuming the output is a
51
51
  copy.
52
52
 
53
+ **Right-to-left text** (Hebrew; Arabic letters are ordered too) is put into
54
+ visual order by `src/bidi.ts`, a hand-written subset of the Unicode
55
+ Bidirectional Algorithm (UAX #9) — hand-written rather than `bidi-js` because
56
+ this package has no dependencies. Levels are resolved per paragraph and each
57
+ wrapped line is reordered on its own; a paragraph whose first letter is
58
+ right-to-left is right-aligned, and its list markers and quote bar move to the
59
+ right. Not done: explicit embedding/isolate controls (ignored), Arabic shaping
60
+ (letters are not joined), and right-to-left column order in tables. An
61
+ all-Latin document's PDF is byte-identical to 0.7.0's — the self-test holds it
62
+ to a hash.
63
+
53
64
  ## Two writers, on purpose
54
65
 
55
66
  The PDF writer here goes through `pdfcore` — base-14 fonts, its own AFM tables,
package/dist/bidi.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ type Level = number;
2
+ /** True if `text` holds a right-to-left letter (or an Arabic digit, or an RLM). */
3
+ export declare function hasRtl(text: string): boolean;
4
+ /** The glyph to draw for `ch` at an odd (right-to-left) level. */
5
+ export declare function mirrorOf(ch: string): string;
6
+ /**
7
+ * The paragraph's direction (P2/P3): 1 if its first strong letter is
8
+ * right-to-left, else 0. A paragraph with no letters at all is left-to-right.
9
+ */
10
+ export declare function paragraphLevel(text: string): 0 | 1;
11
+ export interface BidiParagraph {
12
+ /** The paragraph's own level: 0 left-to-right, 1 right-to-left. */
13
+ base: 0 | 1;
14
+ /** The resolved embedding level of every UTF-16 code unit of the input. */
15
+ levels: Uint8Array;
16
+ }
17
+ /**
18
+ * Resolve the embedding level of every character in one paragraph (rules
19
+ * P2–I2). Returns `null` for a paragraph with nothing right-to-left in it
20
+ * (unless `base` forces it right-to-left), so a caller can keep its all-Latin
21
+ * path exactly as it was.
22
+ *
23
+ * `base` forces the paragraph direction; left out, it comes from the text.
24
+ */
25
+ export declare function resolveLevels(text: string, base?: 0 | 1): BidiParagraph | null;
26
+ /** One thing to place on a line: a base character and the marks on it. */
27
+ export interface BidiCluster {
28
+ text: string;
29
+ level: Level;
30
+ }
31
+ /**
32
+ * Split `text` into clusters — a character plus any combining marks after it —
33
+ * each carrying the level of its first code unit. A mark must be drawn AFTER
34
+ * its base whichever way the line runs (rule L3), so they reverse as one.
35
+ */
36
+ export declare function clustersOf(text: string, levels: ArrayLike<number>): BidiCluster[];
37
+ /**
38
+ * Put one rendered line's clusters into visual (left-to-right) order: rule L1
39
+ * (trailing whitespace and tabs to the paragraph level) then L2 (reverse every
40
+ * run at or above each odd level, highest first). Returns a new array; each
41
+ * item's `level` is the one it was DRAWN at, so the caller can mirror (L4).
42
+ */
43
+ export declare function reorderLine<T extends BidiCluster>(line: readonly T[], base: 0 | 1): T[];
44
+ /**
45
+ * One line of text as it will be SEEN, left to right — the whole algorithm in
46
+ * one call, for a caller that draws a single line (a label, a list marker) and
47
+ * for the tests. Text with nothing right-to-left in it is returned unchanged.
48
+ */
49
+ export declare function visualOrder(text: string, base?: 0 | 1): string;
50
+ export {};
51
+ //# sourceMappingURL=bidi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bidi.d.ts","sourceRoot":"","sources":["../src/bidi.ts"],"names":[],"mappings":"AA+DA,KAAK,KAAK,GAAG,MAAM,CAAA;AAKnB,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5C;AAuJD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,CASlD;AAED,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;IACX,2EAA2E;IAC3E,MAAM,EAAE,UAAU,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,aAAa,GAAG,IAAI,CAqG9E;AA6DD,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,KAAK,CAAA;CACb;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,WAAW,EAAE,CAejF;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CA+BvF;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAM9D"}
package/dist/bidi.js ADDED
@@ -0,0 +1,587 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Right-to-left text, in the order it is SEEN.
3
+ //
4
+ // A PDF content stream draws glyphs left to right, one after another, and that
5
+ // is all it does. Hebrew is stored first-letter-first like every other script,
6
+ // so drawn as stored it comes out mirrored: שלום עולם read backwards. Nothing
7
+ // in a PDF viewer fixes that for you — the reordering is the WRITER's job, and
8
+ // until this file existed nothing here did it.
9
+ //
10
+ // WHY A HAND-WRITTEN SUBSET OF UAX #9 AND NOT `bidi-js`
11
+ // ------------------------------------------------------
12
+ // This package has NO RUNTIME DEPENDENCIES and says so in its README: one of
13
+ // its two consumers (Universal Converter) is sold on being offline from first
14
+ // visit with nothing fetched. `bidi-js` is a fine library, but it is ~60 KB of
15
+ // generated Unicode tables for the whole of UAX #9 — explicit embeddings,
16
+ // isolates, every script — to serve a writer that can only ever draw what its
17
+ // fallback face (Liberation Sans) holds: Latin, Greek, Cyrillic and Hebrew.
18
+ // What that needs is the implicit algorithm on one paragraph, which is short
19
+ // and well specified. So this is exactly that, rule by rule, with the rule
20
+ // names from the standard on each step so it can be checked against it:
21
+ //
22
+ // P2/P3 paragraph direction from the first strong character
23
+ // W1–W7 weak types (marks, numbers, separators)
24
+ // N0 paired brackets
25
+ // N1–N2 neutrals
26
+ // I1–I2 implicit levels
27
+ // L1 trailing whitespace back to the paragraph level (per line)
28
+ // L2 reverse runs, highest level first (per line)
29
+ // L3 combining marks stay AFTER their base (reversed as clusters)
30
+ // L4 mirrored glyphs — '(' in right-to-left text is drawn as ')'
31
+ //
32
+ // NOT DONE, on purpose: explicit embeddings and isolates (LRE/RLE/LRO/RLO/PDF,
33
+ // LRI/RLI/FSI/PDI) are treated as invisible no-ops, and Arabic is ordered but
34
+ // not SHAPED (no joining forms) — Liberation Sans has no Arabic anyway. The
35
+ // character classes cover Latin, Greek, Cyrillic, Hebrew, Arabic and common
36
+ // punctuation; anything unlisted is a strong left-to-right letter, which is the
37
+ // right answer for every other script the suite can draw.
38
+ //
39
+ // ⚠️ LEVELS ARE RESOLVED PER PARAGRAPH, ORDER IS APPLIED PER LINE. The standard
40
+ // is explicit about this and it matters: whether the comma at the end of a
41
+ // wrapped line belongs to the Hebrew before it or the English after it depends
42
+ // on text that may be on the NEXT line. So the writer resolves levels once on
43
+ // the whole paragraph, wraps in logical order, and only then reorders each
44
+ // rendered line — never the other way round, which would wrap a reversed
45
+ // paragraph and put its last words on the first line.
46
+ // ---------------------------------------------------------------------------
47
+ // Bidi classes, as small integers so the per-character arrays are typed arrays.
48
+ const L = 0;
49
+ const R = 1;
50
+ const AL = 2;
51
+ const EN = 3;
52
+ const ES = 4;
53
+ const ET = 5;
54
+ const AN = 6;
55
+ const CS = 7;
56
+ const NSM = 8;
57
+ const BN = 9;
58
+ const B = 10;
59
+ const S = 11;
60
+ const WS = 12;
61
+ const ON = 13;
62
+ /** Anything that can make a paragraph need reordering at all. */
63
+ const RTL_TRIGGER = /[֐-ࣿיִ-﷿ﹰ-ﻼ‏؜]/;
64
+ /** True if `text` holds a right-to-left letter (or an Arabic digit, or an RLM). */
65
+ export function hasRtl(text) {
66
+ return RTL_TRIGGER.test(text);
67
+ }
68
+ function inRange(c, lo, hi) {
69
+ return c >= lo && c <= hi;
70
+ }
71
+ /** Combining marks — they belong to the character before them. */
72
+ function isMark(c) {
73
+ return (inRange(c, 0x0300, 0x036f) ||
74
+ inRange(c, 0x0483, 0x0489) ||
75
+ inRange(c, 0x0591, 0x05bd) ||
76
+ c === 0x05bf ||
77
+ c === 0x05c1 || c === 0x05c2 ||
78
+ c === 0x05c4 || c === 0x05c5 ||
79
+ c === 0x05c7 ||
80
+ inRange(c, 0x0610, 0x061a) ||
81
+ inRange(c, 0x064b, 0x065f) ||
82
+ c === 0x0670 ||
83
+ inRange(c, 0x06d6, 0x06dc) ||
84
+ inRange(c, 0x06df, 0x06e4) ||
85
+ c === 0x06e7 || c === 0x06e8 ||
86
+ inRange(c, 0x06ea, 0x06ed) ||
87
+ inRange(c, 0x08d3, 0x08ff) && c !== 0x08e2 ||
88
+ c === 0xfb1e ||
89
+ inRange(c, 0x20d0, 0x20ff) ||
90
+ inRange(c, 0xfe00, 0xfe0f) ||
91
+ inRange(c, 0xfe20, 0xfe2f));
92
+ }
93
+ /** The Bidi_Class of a code point, for the scripts this writer can draw. */
94
+ function classOf(c) {
95
+ // ASCII first: it is nearly every character in nearly every document.
96
+ if (c < 0x80) {
97
+ if (c >= 0x30 && c <= 0x39)
98
+ return EN;
99
+ if ((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a))
100
+ return L;
101
+ if (c === 0x20 || c === 0x0c)
102
+ return WS;
103
+ if (c === 0x09 || c === 0x0b || c === 0x1f)
104
+ return S;
105
+ if (c === 0x0a || c === 0x0d || (c >= 0x1c && c <= 0x1e))
106
+ return B;
107
+ if (c < 0x20 || c === 0x7f)
108
+ return BN;
109
+ if (c === 0x2b || c === 0x2d)
110
+ return ES; // + -
111
+ if (c >= 0x23 && c <= 0x25)
112
+ return ET; // # $ %
113
+ if (c === 0x2c || c === 0x2e || c === 0x2f || c === 0x3a)
114
+ return CS; // , . / :
115
+ return ON;
116
+ }
117
+ if (isMark(c))
118
+ return NSM;
119
+ // Latin-1 supplement
120
+ if (c < 0x100) {
121
+ if (c === 0x85)
122
+ return B;
123
+ if (c < 0xa0)
124
+ return BN;
125
+ if (c === 0xa0)
126
+ return CS;
127
+ if (c === 0xad)
128
+ return BN;
129
+ if (c === 0xb2 || c === 0xb3 || c === 0xb9)
130
+ return EN;
131
+ if ((c >= 0xa2 && c <= 0xa5) || c === 0xb0 || c === 0xb1)
132
+ return ET;
133
+ if (c === 0xaa || c === 0xb5 || c === 0xba || c >= 0xc0) {
134
+ return c === 0xd7 || c === 0xf7 ? ON : L;
135
+ }
136
+ return ON;
137
+ }
138
+ // Hebrew
139
+ if (c >= 0x0590 && c <= 0x05ff)
140
+ return R;
141
+ // Arabic
142
+ if (c >= 0x0600 && c <= 0x06ff) {
143
+ if (c <= 0x0605 || (c >= 0x0660 && c <= 0x0669) || c === 0x066b || c === 0x066c || c === 0x06dd)
144
+ return AN;
145
+ if (c === 0x060c)
146
+ return CS;
147
+ if (c === 0x066a)
148
+ return ET;
149
+ if (c >= 0x06f0 && c <= 0x06f9)
150
+ return EN;
151
+ if (c === 0x0609 || c === 0x060a)
152
+ return ET;
153
+ if (c === 0x060e || c === 0x060f || c === 0x06de || c === 0x06e9)
154
+ return ON;
155
+ return AL;
156
+ }
157
+ if (c >= 0x0700 && c <= 0x074f)
158
+ return AL; // Syriac
159
+ if (c >= 0x0750 && c <= 0x077f)
160
+ return AL; // Arabic supplement
161
+ if (c >= 0x0780 && c <= 0x07bf)
162
+ return AL; // Thaana
163
+ if (c >= 0x07c0 && c <= 0x085f)
164
+ return R; // NKo, Samaritan, Mandaic
165
+ if (c >= 0x0860 && c <= 0x08ff)
166
+ return AL; // Syriac/Arabic extended
167
+ if (c >= 0xfb1d && c <= 0xfb4f)
168
+ return c === 0xfb29 ? ES : R; // Hebrew presentation forms
169
+ if (c >= 0xfb50 && c <= 0xfdff)
170
+ return c === 0xfd3e || c === 0xfd3f ? ON : AL;
171
+ if (c >= 0xfe70 && c <= 0xfefe)
172
+ return AL;
173
+ if (c === 0xfeff)
174
+ return BN;
175
+ // General punctuation and friends
176
+ if (c >= 0x2000 && c <= 0x200a)
177
+ return WS;
178
+ if (c >= 0x200b && c <= 0x200d)
179
+ return BN;
180
+ if (c === 0x200e)
181
+ return L; // LRM
182
+ if (c === 0x200f)
183
+ return R; // RLM
184
+ if (c === 0x2028)
185
+ return WS;
186
+ if (c === 0x2029)
187
+ return B;
188
+ if ((c >= 0x202a && c <= 0x202e) || (c >= 0x2066 && c <= 0x2069))
189
+ return BN; // embeddings/isolates: unsupported, ignored
190
+ if (c === 0x202f)
191
+ return CS;
192
+ if (c >= 0x2030 && c <= 0x2034)
193
+ return ET;
194
+ if (c === 0x2044)
195
+ return CS;
196
+ if (c === 0x205f)
197
+ return WS;
198
+ if (c >= 0x2060 && c <= 0x2064)
199
+ return BN;
200
+ if (c >= 0x2010 && c <= 0x205e)
201
+ return ON;
202
+ if (c === 0x2070 || (c >= 0x2074 && c <= 0x2079) || (c >= 0x2080 && c <= 0x2089))
203
+ return EN;
204
+ if (c === 0x207a || c === 0x207b || c === 0x208a || c === 0x208b)
205
+ return ES;
206
+ if (c >= 0x207c && c <= 0x207e)
207
+ return ON;
208
+ if (c >= 0x208c && c <= 0x208e)
209
+ return ON;
210
+ if (c >= 0x20a0 && c <= 0x20cf)
211
+ return ET; // currency
212
+ if (c === 0x2212)
213
+ return ES;
214
+ if (c === 0x2213)
215
+ return ET;
216
+ if (c === 0x212e)
217
+ return ET;
218
+ if (c >= 0x2100 && c <= 0x214f)
219
+ return L; // letterlike: mostly L, a few ON — L is the safe default
220
+ if (c >= 0x2150 && c <= 0x218f)
221
+ return ON;
222
+ if (c >= 0x2190 && c <= 0x2bff)
223
+ return ON; // arrows, maths, box drawing, shapes, dingbats
224
+ if (c === 0x1680 || c === 0x3000)
225
+ return WS;
226
+ if (c >= 0x3001 && c <= 0x3004)
227
+ return ON;
228
+ if (c >= 0x3008 && c <= 0x3020)
229
+ return ON;
230
+ if (c >= 0xfe50 && c <= 0xfe6f) {
231
+ if (c === 0xfe50 || c === 0xfe52 || c === 0xfe55)
232
+ return CS;
233
+ if (c === 0xfe62 || c === 0xfe63)
234
+ return ES;
235
+ if (c === 0xfe5f || c === 0xfe69 || c === 0xfe6a)
236
+ return ET;
237
+ return ON;
238
+ }
239
+ if (c >= 0xff01 && c <= 0xff20) {
240
+ if (c >= 0xff10 && c <= 0xff19)
241
+ return EN;
242
+ if (c === 0xff0b || c === 0xff0d)
243
+ return ES;
244
+ if (c >= 0xff03 && c <= 0xff05)
245
+ return ET;
246
+ if (c === 0xff0c || c === 0xff0e || c === 0xff0f || c === 0xff1a)
247
+ return CS;
248
+ return ON;
249
+ }
250
+ if (c >= 0x10800 && c <= 0x10fff)
251
+ return R; // historic RTL scripts
252
+ if (c >= 0x1e800 && c <= 0x1efff)
253
+ return R;
254
+ return L;
255
+ }
256
+ /** Bidi_Mirroring_Glyph for the pairs a document is likely to hold. */
257
+ const MIRROR = {
258
+ '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<',
259
+ '\u00ab': '\u00bb', '\u00bb': '\u00ab', // « »
260
+ '\u2039': '\u203a', '\u203a': '\u2039', // ‹ ›
261
+ '\u2264': '\u2265', '\u2265': '\u2264', // ≤ ≥
262
+ '\u226a': '\u226b', '\u226b': '\u226a', // ≪ ≫
263
+ '\u2045': '\u2046', '\u2046': '\u2045', // ⁅ ⁆
264
+ '\u207d': '\u207e', '\u207e': '\u207d', // superscript ( )
265
+ '\u208d': '\u208e', '\u208e': '\u208d', // subscript ( )
266
+ '\u2329': '\u232a', '\u232a': '\u2329', // angle brackets
267
+ '\u3008': '\u3009', '\u3009': '\u3008',
268
+ };
269
+ /** Bidi_Paired_Bracket, opening → closing, for rule N0. */
270
+ const OPENING = {
271
+ '(': ')', '[': ']', '{': '}',
272
+ '\u2045': '\u2046', '\u207d': '\u207e', '\u208d': '\u208e', '\u2329': '\u232a', '\u3008': '\u3009',
273
+ };
274
+ const CLOSING = new Set(Object.values(OPENING));
275
+ /** The glyph to draw for `ch` at an odd (right-to-left) level. */
276
+ export function mirrorOf(ch) {
277
+ return MIRROR[ch] ?? ch;
278
+ }
279
+ /**
280
+ * The paragraph's direction (P2/P3): 1 if its first strong letter is
281
+ * right-to-left, else 0. A paragraph with no letters at all is left-to-right.
282
+ */
283
+ export function paragraphLevel(text) {
284
+ for (let i = 0; i < text.length; i += 1) {
285
+ const cp = text.codePointAt(i);
286
+ if (cp > 0xffff)
287
+ i += 1;
288
+ const t = classOf(cp);
289
+ if (t === L)
290
+ return 0;
291
+ if (t === R || t === AL)
292
+ return 1;
293
+ }
294
+ return 0;
295
+ }
296
+ /**
297
+ * Resolve the embedding level of every character in one paragraph (rules
298
+ * P2–I2). Returns `null` for a paragraph with nothing right-to-left in it
299
+ * (unless `base` forces it right-to-left), so a caller can keep its all-Latin
300
+ * path exactly as it was.
301
+ *
302
+ * `base` forces the paragraph direction; left out, it comes from the text.
303
+ */
304
+ export function resolveLevels(text, base) {
305
+ // A paragraph FORCED right-to-left needs resolving even with no Hebrew in
306
+ // it: "1." in a Hebrew list reads ".1".
307
+ if (base !== 1 && !hasRtl(text))
308
+ return null;
309
+ const e = base ?? paragraphLevel(text);
310
+ const n = text.length;
311
+ const orig = new Uint8Array(n);
312
+ for (let i = 0; i < n; i += 1) {
313
+ const cp = text.codePointAt(i);
314
+ orig[i] = classOf(cp);
315
+ if (cp > 0xffff) {
316
+ // The low surrogate travels with its high one.
317
+ orig[i + 1] = orig[i] === NSM ? NSM : orig[i];
318
+ i += 1;
319
+ }
320
+ }
321
+ // A surrogate pair's second half is not a character of its own: treated as a
322
+ // mark, it simply takes whatever its first half resolves to.
323
+ const types = new Uint8Array(orig);
324
+ for (let i = 0; i < n; i += 1) {
325
+ const cp = text.charCodeAt(i);
326
+ if (cp >= 0xdc00 && cp <= 0xdfff && i > 0)
327
+ types[i] = NSM;
328
+ }
329
+ const sos = e === 1 ? R : L;
330
+ const eos = sos;
331
+ // X9 (approximately): boundary-neutral characters behave like marks, taking
332
+ // the type of what they follow. Removing them outright would shift every
333
+ // index after them; this keeps the arrays aligned with the text.
334
+ for (let i = 0; i < n; i += 1)
335
+ if (types[i] === BN)
336
+ types[i] = NSM;
337
+ // W1: a mark takes the type of the character before it.
338
+ for (let i = 0; i < n; i += 1) {
339
+ if (types[i] === NSM)
340
+ types[i] = i === 0 ? sos : types[i - 1];
341
+ }
342
+ // W2: European digits after Arabic letters are Arabic digits.
343
+ let lastStrong = sos;
344
+ for (let i = 0; i < n; i += 1) {
345
+ const t = types[i];
346
+ if (t === L || t === R || t === AL)
347
+ lastStrong = t;
348
+ else if (t === EN && lastStrong === AL)
349
+ types[i] = AN;
350
+ }
351
+ // W3: Arabic letters are right-to-left letters from here on.
352
+ for (let i = 0; i < n; i += 1)
353
+ if (types[i] === AL)
354
+ types[i] = R;
355
+ // W4: one separator between two numbers of the same kind joins them.
356
+ for (let i = 1; i < n - 1; i += 1) {
357
+ const t = types[i];
358
+ const prev = types[i - 1];
359
+ const next = types[i + 1];
360
+ if (t === ES && prev === EN && next === EN)
361
+ types[i] = EN;
362
+ else if (t === CS && prev === EN && next === EN)
363
+ types[i] = EN;
364
+ else if (t === CS && prev === AN && next === AN)
365
+ types[i] = AN;
366
+ }
367
+ // W5: currency and percent signs touching a European number join it.
368
+ for (let i = 0; i < n; i += 1) {
369
+ if (types[i] !== ET)
370
+ continue;
371
+ let j = i;
372
+ while (j < n && types[j] === ET)
373
+ j += 1;
374
+ const touches = (i > 0 && types[i - 1] === EN) || (j < n && types[j] === EN);
375
+ if (touches)
376
+ for (let k = i; k < j; k += 1)
377
+ types[k] = EN;
378
+ i = j - 1;
379
+ }
380
+ // W6: any separator left over is a neutral.
381
+ for (let i = 0; i < n; i += 1) {
382
+ const t = types[i];
383
+ if (t === ES || t === ET || t === CS)
384
+ types[i] = ON;
385
+ }
386
+ // W7: European numbers after a left-to-right letter are left-to-right.
387
+ lastStrong = sos;
388
+ for (let i = 0; i < n; i += 1) {
389
+ const t = types[i];
390
+ if (t === L || t === R)
391
+ lastStrong = t;
392
+ else if (t === EN && lastStrong === L)
393
+ types[i] = L;
394
+ }
395
+ // N0: paired brackets resolve together, from what they enclose.
396
+ resolveBrackets(text, orig, types, e, sos);
397
+ // N1/N2: a run of neutrals between two things of the same direction takes
398
+ // that direction; otherwise the paragraph's. Numbers count as right-to-left.
399
+ const isNeutral = (t) => t === ON || t === WS || t === S || t === B;
400
+ const strongDir = (t) => (t === L ? L : R); // only called on L, R, EN, AN
401
+ for (let i = 0; i < n; i += 1) {
402
+ if (!isNeutral(types[i]))
403
+ continue;
404
+ let j = i;
405
+ while (j < n && isNeutral(types[j]))
406
+ j += 1;
407
+ const before = i === 0 ? sos : strongDir(types[i - 1]);
408
+ const after = j === n ? eos : strongDir(types[j]);
409
+ const dir = before === after ? before : e === 1 ? R : L;
410
+ for (let k = i; k < j; k += 1)
411
+ types[k] = dir;
412
+ i = j - 1;
413
+ }
414
+ // I1/I2: implicit levels.
415
+ const levels = new Uint8Array(n);
416
+ for (let i = 0; i < n; i += 1) {
417
+ const t = types[i];
418
+ if (e === 0)
419
+ levels[i] = t === R ? 1 : t === AN || t === EN ? 2 : 0;
420
+ else
421
+ levels[i] = t === L || t === EN || t === AN ? 2 : 1;
422
+ }
423
+ return { base: e, levels };
424
+ }
425
+ /**
426
+ * Rule N0 (UAX #9 §3.3.5), with BD16's pairing: a bracket pair takes the
427
+ * paragraph's direction if it encloses anything of that direction, else the
428
+ * opposite one if it encloses that AND the text before it agrees.
429
+ */
430
+ function resolveBrackets(text, orig, types, e, sos) {
431
+ const pairs = [];
432
+ const stack = [];
433
+ for (let i = 0; i < text.length; i += 1) {
434
+ if (types[i] !== ON)
435
+ continue;
436
+ const ch = text[i];
437
+ const close = OPENING[ch];
438
+ if (close) {
439
+ if (stack.length === 63)
440
+ break; // BD16: stop at the stack limit
441
+ stack.push({ close, at: i });
442
+ }
443
+ else if (CLOSING.has(ch)) {
444
+ for (let s = stack.length - 1; s >= 0; s -= 1) {
445
+ if (stack[s].close === ch) {
446
+ pairs.push([stack[s].at, i]);
447
+ stack.length = s;
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ }
453
+ pairs.sort((a, b) => a[0] - b[0]);
454
+ const embed = e === 1 ? R : L;
455
+ const opposite = e === 1 ? L : R;
456
+ const strongOf = (t) => t === L ? L : t === R || t === AL || t === EN || t === AN ? R : null;
457
+ for (const [open, close] of pairs) {
458
+ let foundEmbed = false;
459
+ let foundOpposite = false;
460
+ for (let k = open + 1; k < close; k += 1) {
461
+ const s = strongOf(types[k]);
462
+ if (s === embed) {
463
+ foundEmbed = true;
464
+ break;
465
+ }
466
+ if (s === opposite)
467
+ foundOpposite = true;
468
+ }
469
+ let dir = null;
470
+ if (foundEmbed)
471
+ dir = embed;
472
+ else if (foundOpposite) {
473
+ let before = sos;
474
+ for (let k = open - 1; k >= 0; k -= 1) {
475
+ const s = strongOf(types[k]);
476
+ if (s !== null) {
477
+ before = s;
478
+ break;
479
+ }
480
+ }
481
+ dir = before === opposite ? opposite : embed;
482
+ }
483
+ if (dir === null)
484
+ continue;
485
+ for (const at of [open, close]) {
486
+ types[at] = dir;
487
+ // Marks on a bracket follow it (they were made ON by W1).
488
+ for (let k = at + 1; k < text.length && orig[k] === NSM; k += 1)
489
+ types[k] = dir;
490
+ }
491
+ }
492
+ }
493
+ /**
494
+ * Split `text` into clusters — a character plus any combining marks after it —
495
+ * each carrying the level of its first code unit. A mark must be drawn AFTER
496
+ * its base whichever way the line runs (rule L3), so they reverse as one.
497
+ */
498
+ export function clustersOf(text, levels) {
499
+ const out = [];
500
+ let i = 0;
501
+ while (i < text.length) {
502
+ const start = i;
503
+ const cp = text.codePointAt(i);
504
+ i += cp > 0xffff ? 2 : 1;
505
+ while (i < text.length) {
506
+ const next = text.codePointAt(i);
507
+ if (!isMark(next) && next !== 0x200d)
508
+ break;
509
+ i += next > 0xffff ? 2 : 1;
510
+ }
511
+ out.push({ text: text.slice(start, i), level: levels[start] ?? 0 });
512
+ }
513
+ return out;
514
+ }
515
+ const WHITESPACE = /^\s+$/; // \s includes U+2028, which stands in for a hard break
516
+ /**
517
+ * Put one rendered line's clusters into visual (left-to-right) order: rule L1
518
+ * (trailing whitespace and tabs to the paragraph level) then L2 (reverse every
519
+ * run at or above each odd level, highest first). Returns a new array; each
520
+ * item's `level` is the one it was DRAWN at, so the caller can mirror (L4).
521
+ */
522
+ export function reorderLine(line, base) {
523
+ const items = line.map((c) => ({ ...c }));
524
+ // L1: tabs, whitespace before a tab, and whitespace at the end of the line.
525
+ let trailing = true;
526
+ for (let i = items.length - 1; i >= 0; i -= 1) {
527
+ const t = items[i].text;
528
+ if (t === '\t') {
529
+ items[i].level = base;
530
+ trailing = true;
531
+ continue;
532
+ }
533
+ if (WHITESPACE.test(t)) {
534
+ if (trailing)
535
+ items[i].level = base;
536
+ continue;
537
+ }
538
+ trailing = false;
539
+ }
540
+ // L2
541
+ let highest = 0;
542
+ let lowestOdd = 255;
543
+ for (const c of items) {
544
+ if (c.level > highest)
545
+ highest = c.level;
546
+ if (c.level % 2 === 1 && c.level < lowestOdd)
547
+ lowestOdd = c.level;
548
+ }
549
+ for (let level = highest; level >= lowestOdd && level > 0; level -= 1) {
550
+ let i = 0;
551
+ while (i < items.length) {
552
+ if (items[i].level < level) {
553
+ i += 1;
554
+ continue;
555
+ }
556
+ let j = i;
557
+ while (j < items.length && items[j].level >= level)
558
+ j += 1;
559
+ reverse(items, i, j - 1);
560
+ i = j;
561
+ }
562
+ }
563
+ return items;
564
+ }
565
+ function reverse(a, from, to) {
566
+ while (from < to) {
567
+ const t = a[from];
568
+ a[from] = a[to];
569
+ a[to] = t;
570
+ from += 1;
571
+ to -= 1;
572
+ }
573
+ }
574
+ /**
575
+ * One line of text as it will be SEEN, left to right — the whole algorithm in
576
+ * one call, for a caller that draws a single line (a label, a list marker) and
577
+ * for the tests. Text with nothing right-to-left in it is returned unchanged.
578
+ */
579
+ export function visualOrder(text, base) {
580
+ const para = resolveLevels(text, base);
581
+ if (!para)
582
+ return text;
583
+ return reorderLine(clustersOf(text, para.levels), para.base)
584
+ .map((c) => (c.level % 2 === 1 ? mirrorOf(c.text) : c.text))
585
+ .join('');
586
+ }
587
+ //# sourceMappingURL=bidi.js.map