@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 +11 -0
- package/dist/bidi.d.ts +51 -0
- package/dist/bidi.d.ts.map +1 -0
- package/dist/bidi.js +587 -0
- package/dist/bidi.js.map +1 -0
- package/dist/convert.d.ts +2 -1
- package/dist/convert.d.ts.map +1 -1
- package/dist/convert.js +4 -3
- package/dist/convert.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/pdfcore.d.ts +45 -3
- package/dist/pdfcore.d.ts.map +1 -1
- package/dist/pdfcore.js +300 -32
- package/dist/pdfcore.js.map +1 -1
- package/dist/truetype.d.ts +33 -0
- package/dist/truetype.d.ts.map +1 -0
- package/dist/truetype.js +289 -0
- package/dist/truetype.js.map +1 -0
- package/dist/write/pdf.d.ts +17 -1
- package/dist/write/pdf.d.ts.map +1 -1
- package/dist/write/pdf.js +193 -23
- package/dist/write/pdf.js.map +1 -1
- package/package.json +1 -1
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
|