react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/compose.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
// Composition: dead keys and the Compose key, client-side.
|
|
2
|
+
//
|
|
3
|
+
// A key event carries one keysym and at most one code point, so the path
|
|
4
|
+
// from `EventManager._onKey` to `TextInputNode._insert` could only ever type
|
|
5
|
+
// what one key produces. That is enough for a US layout and for nothing
|
|
6
|
+
// else: `é` on a French, German or us-intl layout is `dead_acute` then `e`,
|
|
7
|
+
// and `dead_acute` is a keysym with no code point at all — it used to arrive
|
|
8
|
+
// and be dropped, so the accent vanished and the letter came out bare
|
|
9
|
+
// (issue #272).
|
|
10
|
+
//
|
|
11
|
+
// This is the state that was missing between those two points. It is
|
|
12
|
+
// entirely client-side: no X extension, no input-method process, no
|
|
13
|
+
// protocol. `XK_dead_*` and `XK_Multi_key` are ordinary keysyms that already
|
|
14
|
+
// arrive; what turns a *sequence* of them into a character is a table and a
|
|
15
|
+
// small machine in front of the insert.
|
|
16
|
+
//
|
|
17
|
+
// ## The table is Unicode's, not ours
|
|
18
|
+
//
|
|
19
|
+
// The obvious implementation is X's `Compose` file: six thousand lines
|
|
20
|
+
// mapping keysym sequences to strings. Bundling it would be 300 kB of data
|
|
21
|
+
// for the Latin coverage alone, and it would still be a snapshot of one
|
|
22
|
+
// machine's locale.
|
|
23
|
+
//
|
|
24
|
+
// A dead key *is* a combining mark, so the composition is the one Unicode
|
|
25
|
+
// already specifies: `dead_acute` is U+0301, and `dead_acute` + `e` is
|
|
26
|
+
// `'é'.normalize('NFC')`. That is 30 entries instead of 6000, it
|
|
27
|
+
// covers every base letter in every script — Cyrillic, Greek, Vietnamese
|
|
28
|
+
// with two stacked marks — and it cannot drift, because the data is the
|
|
29
|
+
// runtime's. Only the sequences Unicode has no canonical composition for
|
|
30
|
+
// need naming by hand: `dead_stroke` + `o` is `ø`, whose decomposition
|
|
31
|
+
// Unicode deliberately does not define, and the `Multi_key` symbol
|
|
32
|
+
// sequences (`Compose o c` is `©`), which are conventions rather than
|
|
33
|
+
// characters.
|
|
34
|
+
//
|
|
35
|
+
// ## Default, and the seam
|
|
36
|
+
//
|
|
37
|
+
// The built-in table is what an app gets with no configuration, and it is
|
|
38
|
+
// chosen to cover what a Latin-script keyboard can type: every dead key on
|
|
39
|
+
// every common layout, plus the punctuation and symbol sequences people
|
|
40
|
+
// actually reach for. The seam is `createRoot({ compose })` — `'system'`
|
|
41
|
+
// adds the machine's own Compose file (which is what picks up a personal
|
|
42
|
+
// `~/.XCompose`), `{ sequences }` adds or overrides individual ones, and
|
|
43
|
+
// `false` turns the whole thing off for an app that means to do its own.
|
|
44
|
+
//
|
|
45
|
+
// What this does *not* do is an input method: there is no preedit coming
|
|
46
|
+
// from another process, no candidate window, and therefore no CJK. That is
|
|
47
|
+
// XIM or an IBus/Fcitx D-Bus client, and it is the next tier of #272 — but
|
|
48
|
+
// it lands on this machinery rather than beside it, because the preedit
|
|
49
|
+
// buffer and the composition events it needs are the ones below.
|
|
50
|
+
import { readFileSync } from 'node:fs';
|
|
51
|
+
import { homedir } from 'node:os';
|
|
52
|
+
import { join } from 'node:path';
|
|
53
|
+
import {
|
|
54
|
+
charOf,
|
|
55
|
+
keysymOf,
|
|
56
|
+
isDeadKeysym,
|
|
57
|
+
XK_MULTI_KEY,
|
|
58
|
+
XK_ESCAPE,
|
|
59
|
+
XK_BACKSPACE,
|
|
60
|
+
XK_SPACE,
|
|
61
|
+
XK_DEAD_GRAVE,
|
|
62
|
+
XK_DEAD_ACUTE,
|
|
63
|
+
XK_DEAD_CIRCUMFLEX,
|
|
64
|
+
XK_DEAD_TILDE,
|
|
65
|
+
XK_DEAD_MACRON,
|
|
66
|
+
XK_DEAD_BREVE,
|
|
67
|
+
XK_DEAD_ABOVEDOT,
|
|
68
|
+
XK_DEAD_DIAERESIS,
|
|
69
|
+
XK_DEAD_ABOVERING,
|
|
70
|
+
XK_DEAD_DOUBLEACUTE,
|
|
71
|
+
XK_DEAD_CARON,
|
|
72
|
+
XK_DEAD_CEDILLA,
|
|
73
|
+
XK_DEAD_OGONEK,
|
|
74
|
+
XK_DEAD_IOTA,
|
|
75
|
+
XK_DEAD_VOICED_SOUND,
|
|
76
|
+
XK_DEAD_SEMIVOICED_SOUND,
|
|
77
|
+
XK_DEAD_BELOWDOT,
|
|
78
|
+
XK_DEAD_HOOK,
|
|
79
|
+
XK_DEAD_HORN,
|
|
80
|
+
XK_DEAD_STROKE,
|
|
81
|
+
XK_DEAD_ABOVECOMMA,
|
|
82
|
+
XK_DEAD_ABOVEREVERSEDCOMMA,
|
|
83
|
+
XK_DEAD_DOUBLEGRAVE,
|
|
84
|
+
XK_DEAD_BELOWRING,
|
|
85
|
+
XK_DEAD_BELOWMACRON,
|
|
86
|
+
XK_DEAD_BELOWCIRCUMFLEX,
|
|
87
|
+
XK_DEAD_BELOWTILDE,
|
|
88
|
+
XK_DEAD_BELOWBREVE,
|
|
89
|
+
XK_DEAD_BELOWDIAERESIS,
|
|
90
|
+
XK_DEAD_INVERTEDBREVE,
|
|
91
|
+
XK_DEAD_BELOWCOMMA,
|
|
92
|
+
XK_DEAD_CURRENCY,
|
|
93
|
+
} from './keysyms.js';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The combining mark each dead key applies. This is the whole dead-key
|
|
97
|
+
* table: everything else about `dead_acute` + `e` follows from Unicode
|
|
98
|
+
* normalisation.
|
|
99
|
+
*/
|
|
100
|
+
const MARKS = {
|
|
101
|
+
[XK_DEAD_GRAVE]: '̀',
|
|
102
|
+
[XK_DEAD_ACUTE]: '́',
|
|
103
|
+
[XK_DEAD_CIRCUMFLEX]: '̂',
|
|
104
|
+
[XK_DEAD_TILDE]: '̃',
|
|
105
|
+
[XK_DEAD_MACRON]: '̄',
|
|
106
|
+
[XK_DEAD_BREVE]: '̆',
|
|
107
|
+
[XK_DEAD_ABOVEDOT]: '̇',
|
|
108
|
+
[XK_DEAD_DIAERESIS]: '̈',
|
|
109
|
+
[XK_DEAD_HOOK]: '̉',
|
|
110
|
+
[XK_DEAD_ABOVERING]: '̊',
|
|
111
|
+
[XK_DEAD_DOUBLEACUTE]: '̋',
|
|
112
|
+
[XK_DEAD_CARON]: '̌',
|
|
113
|
+
[XK_DEAD_DOUBLEGRAVE]: '̏',
|
|
114
|
+
[XK_DEAD_INVERTEDBREVE]: '̑',
|
|
115
|
+
[XK_DEAD_ABOVECOMMA]: '̓',
|
|
116
|
+
[XK_DEAD_ABOVEREVERSEDCOMMA]: '̔',
|
|
117
|
+
[XK_DEAD_HORN]: '̛',
|
|
118
|
+
[XK_DEAD_BELOWDOT]: '̣',
|
|
119
|
+
[XK_DEAD_BELOWDIAERESIS]: '̤',
|
|
120
|
+
[XK_DEAD_BELOWRING]: '̥',
|
|
121
|
+
[XK_DEAD_BELOWCOMMA]: '̦',
|
|
122
|
+
[XK_DEAD_CEDILLA]: '̧',
|
|
123
|
+
[XK_DEAD_OGONEK]: '̨',
|
|
124
|
+
[XK_DEAD_BELOWCIRCUMFLEX]: '̭',
|
|
125
|
+
[XK_DEAD_BELOWBREVE]: '̮',
|
|
126
|
+
[XK_DEAD_BELOWTILDE]: '̰',
|
|
127
|
+
[XK_DEAD_BELOWMACRON]: '̱',
|
|
128
|
+
[XK_DEAD_STROKE]: '̸',
|
|
129
|
+
[XK_DEAD_IOTA]: 'ͅ',
|
|
130
|
+
[XK_DEAD_VOICED_SOUND]: '゙',
|
|
131
|
+
[XK_DEAD_SEMIVOICED_SOUND]: '゚',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* What a dead key shows while it is pending, and what it types on its own —
|
|
136
|
+
* `dead_acute` then space is `´`, which is how the accent character itself
|
|
137
|
+
* is typed on a layout that has no separate key for it.
|
|
138
|
+
*
|
|
139
|
+
* A mark with no spacing form of its own falls back to the mark on a space,
|
|
140
|
+
* which renders as the mark: not beautiful in every font, but it is the
|
|
141
|
+
* accent, which is what the user needs to see.
|
|
142
|
+
*/
|
|
143
|
+
const SPACING = {
|
|
144
|
+
[XK_DEAD_GRAVE]: '`',
|
|
145
|
+
[XK_DEAD_ACUTE]: '´',
|
|
146
|
+
[XK_DEAD_CIRCUMFLEX]: '^',
|
|
147
|
+
[XK_DEAD_TILDE]: '~',
|
|
148
|
+
[XK_DEAD_MACRON]: '¯',
|
|
149
|
+
[XK_DEAD_BREVE]: '˘',
|
|
150
|
+
[XK_DEAD_ABOVEDOT]: '˙',
|
|
151
|
+
[XK_DEAD_DIAERESIS]: '¨',
|
|
152
|
+
[XK_DEAD_ABOVERING]: '°',
|
|
153
|
+
[XK_DEAD_DOUBLEACUTE]: '˝',
|
|
154
|
+
[XK_DEAD_CARON]: 'ˇ',
|
|
155
|
+
[XK_DEAD_CEDILLA]: '¸',
|
|
156
|
+
[XK_DEAD_OGONEK]: '˛',
|
|
157
|
+
[XK_DEAD_STROKE]: '/',
|
|
158
|
+
[XK_DEAD_CURRENCY]: '¤',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The pairs Unicode will not compose, because the character has no
|
|
163
|
+
* canonical decomposition: a stroke through a letter is a different letter,
|
|
164
|
+
* not a decorated one, and a currency sign is not a decorated letter at all.
|
|
165
|
+
* Small and closed, which is the test for what belongs here.
|
|
166
|
+
*/
|
|
167
|
+
const UNCOMPOSED = {
|
|
168
|
+
[XK_DEAD_STROKE]: {
|
|
169
|
+
o: 'ø',
|
|
170
|
+
O: 'Ø',
|
|
171
|
+
d: 'đ',
|
|
172
|
+
D: 'Đ',
|
|
173
|
+
l: 'ł',
|
|
174
|
+
L: 'Ł',
|
|
175
|
+
t: 'ŧ',
|
|
176
|
+
T: 'Ŧ',
|
|
177
|
+
h: 'ħ',
|
|
178
|
+
H: 'Ħ',
|
|
179
|
+
b: 'ƀ',
|
|
180
|
+
B: 'Ƀ',
|
|
181
|
+
g: 'ǥ',
|
|
182
|
+
G: 'Ǥ',
|
|
183
|
+
i: 'ɨ',
|
|
184
|
+
I: 'Ɨ',
|
|
185
|
+
},
|
|
186
|
+
[XK_DEAD_CURRENCY]: {
|
|
187
|
+
e: '€',
|
|
188
|
+
E: '€',
|
|
189
|
+
l: '£',
|
|
190
|
+
L: '£',
|
|
191
|
+
y: '¥',
|
|
192
|
+
Y: '¥',
|
|
193
|
+
c: '¢',
|
|
194
|
+
C: '¢',
|
|
195
|
+
r: '₹',
|
|
196
|
+
R: '₹',
|
|
197
|
+
w: '₩',
|
|
198
|
+
W: '₩',
|
|
199
|
+
f: '₣',
|
|
200
|
+
F: '₣',
|
|
201
|
+
d: '$',
|
|
202
|
+
D: '$',
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A character that names an accent when it follows `Multi_key`. This is
|
|
208
|
+
* what makes `Compose ' e` type `é` without a table entry per letter: the
|
|
209
|
+
* quote resolves to `dead_acute` and the ordinary dead-key path takes it
|
|
210
|
+
* from there, in either order (`Compose e '` too, which is how half the
|
|
211
|
+
* standard Compose file is written).
|
|
212
|
+
*/
|
|
213
|
+
const ACCENTS = {
|
|
214
|
+
"'": XK_DEAD_ACUTE,
|
|
215
|
+
'`': XK_DEAD_GRAVE,
|
|
216
|
+
'^': XK_DEAD_CIRCUMFLEX,
|
|
217
|
+
'~': XK_DEAD_TILDE,
|
|
218
|
+
'"': XK_DEAD_DIAERESIS,
|
|
219
|
+
',': XK_DEAD_CEDILLA,
|
|
220
|
+
';': XK_DEAD_OGONEK,
|
|
221
|
+
_: XK_DEAD_MACRON,
|
|
222
|
+
'.': XK_DEAD_ABOVEDOT,
|
|
223
|
+
'/': XK_DEAD_STROKE,
|
|
224
|
+
o: XK_DEAD_ABOVERING,
|
|
225
|
+
v: XK_DEAD_CARON,
|
|
226
|
+
U: XK_DEAD_BREVE,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The `Multi_key` sequences, keyed by the characters that follow it. These
|
|
231
|
+
* are the ones that are conventions rather than compositions — no rule
|
|
232
|
+
* derives `©` from `o` and `c` — so they are named, and the list is the
|
|
233
|
+
* part of X's Compose file people actually press. Anything reachable
|
|
234
|
+
* through an accent (`Compose ' e`) is deliberately absent: `ACCENTS`
|
|
235
|
+
* covers those by rule.
|
|
236
|
+
*/
|
|
237
|
+
const SYMBOLS = {
|
|
238
|
+
oc: '©',
|
|
239
|
+
oC: '©',
|
|
240
|
+
Oc: '©',
|
|
241
|
+
OC: '©',
|
|
242
|
+
or: '®',
|
|
243
|
+
oR: '®',
|
|
244
|
+
Or: '®',
|
|
245
|
+
OR: '®',
|
|
246
|
+
tm: '™',
|
|
247
|
+
TM: '™',
|
|
248
|
+
'+-': '±',
|
|
249
|
+
xx: '×',
|
|
250
|
+
':-': '÷',
|
|
251
|
+
'-:': '÷',
|
|
252
|
+
oo: '°',
|
|
253
|
+
'..': '…',
|
|
254
|
+
'--.': '–',
|
|
255
|
+
'---': '—',
|
|
256
|
+
'<<': '«',
|
|
257
|
+
'>>': '»',
|
|
258
|
+
'"<': '“',
|
|
259
|
+
'<"': '“',
|
|
260
|
+
'">': '”',
|
|
261
|
+
'>"': '”',
|
|
262
|
+
"'<": '‘',
|
|
263
|
+
"<'": '‘',
|
|
264
|
+
"'>": '’',
|
|
265
|
+
">'": '’',
|
|
266
|
+
'??': '¿',
|
|
267
|
+
'!!': '¡',
|
|
268
|
+
ss: 'ß',
|
|
269
|
+
SS: 'ẞ',
|
|
270
|
+
ae: 'æ',
|
|
271
|
+
AE: 'Æ',
|
|
272
|
+
oe: 'œ',
|
|
273
|
+
OE: 'Œ',
|
|
274
|
+
12: '½',
|
|
275
|
+
13: '⅓',
|
|
276
|
+
14: '¼',
|
|
277
|
+
34: '¾',
|
|
278
|
+
'=e': '€',
|
|
279
|
+
'=E': '€',
|
|
280
|
+
'=l': '£',
|
|
281
|
+
'=L': '£',
|
|
282
|
+
'=y': '¥',
|
|
283
|
+
'=Y': '¥',
|
|
284
|
+
'=r': '₹',
|
|
285
|
+
'=R': '₹',
|
|
286
|
+
'=w': '₩',
|
|
287
|
+
'=W': '₩',
|
|
288
|
+
'c/': '¢',
|
|
289
|
+
'/c': '¢',
|
|
290
|
+
'->': '→',
|
|
291
|
+
'<-': '←',
|
|
292
|
+
'<=': '≤',
|
|
293
|
+
'>=': '≥',
|
|
294
|
+
'%o': '‰',
|
|
295
|
+
'/u': 'µ',
|
|
296
|
+
mu: 'µ',
|
|
297
|
+
'.=': '•',
|
|
298
|
+
so: '§',
|
|
299
|
+
'p!': '¶',
|
|
300
|
+
'P!': '¶',
|
|
301
|
+
'+z': '†',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* What the Compose key itself shows while a sequence is open.
|
|
306
|
+
*
|
|
307
|
+
* Not nothing: pressing Compose and seeing the field sit there is the
|
|
308
|
+
* "answer the input, not the outcome" failure (AGENTS.md) in its purest
|
|
309
|
+
* form — the key did something, and the only evidence is that the next
|
|
310
|
+
* two keys will behave strangely.
|
|
311
|
+
*/
|
|
312
|
+
const COMPOSE_MARK = '·';
|
|
313
|
+
|
|
314
|
+
/** A sequence trie: keysym arrays in, `{ text, prefix }` out. */
|
|
315
|
+
class ComposeTable {
|
|
316
|
+
constructor() {
|
|
317
|
+
this.root = { children: new Map(), text: undefined };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Later definitions win, which is what makes a Compose file an override
|
|
321
|
+
* of the built-ins rather than an addition beside them. */
|
|
322
|
+
add(keysyms, text) {
|
|
323
|
+
let node = this.root;
|
|
324
|
+
for (const k of keysyms) {
|
|
325
|
+
let next = node.children.get(k);
|
|
326
|
+
if (!next) {
|
|
327
|
+
next = { children: new Map(), text: undefined };
|
|
328
|
+
node.children.set(k, next);
|
|
329
|
+
}
|
|
330
|
+
node = next;
|
|
331
|
+
}
|
|
332
|
+
node.text = text;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
lookup(keysyms) {
|
|
336
|
+
let node = this.root;
|
|
337
|
+
for (const k of keysyms) {
|
|
338
|
+
node = node.children.get(k);
|
|
339
|
+
if (!node) return { text: undefined, prefix: false };
|
|
340
|
+
}
|
|
341
|
+
return { text: node.text, prefix: node.children.size > 0 };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let builtinTable = null;
|
|
346
|
+
|
|
347
|
+
/** The table every app gets without asking. Built once, lazily. */
|
|
348
|
+
export function builtinCompose() {
|
|
349
|
+
if (builtinTable) return builtinTable;
|
|
350
|
+
const table = new ComposeTable();
|
|
351
|
+
for (const [seq, text] of Object.entries(SYMBOLS)) {
|
|
352
|
+
table.add([XK_MULTI_KEY, ...Array.from(seq, keysymOf)], text);
|
|
353
|
+
}
|
|
354
|
+
builtinTable = table;
|
|
355
|
+
return table;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --- Compose files ---------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* X keysym names for the ASCII range, which is what a Compose file's input
|
|
362
|
+
* side is written in. Generated rather than listed: the letters and digits
|
|
363
|
+
* name themselves, and only the punctuation has names to remember.
|
|
364
|
+
*/
|
|
365
|
+
const ASCII_NAMES = (() => {
|
|
366
|
+
const names = new Map();
|
|
367
|
+
const punctuation = {
|
|
368
|
+
0x20: 'space',
|
|
369
|
+
0x21: 'exclam',
|
|
370
|
+
0x22: 'quotedbl',
|
|
371
|
+
0x23: 'numbersign',
|
|
372
|
+
0x24: 'dollar',
|
|
373
|
+
0x25: 'percent',
|
|
374
|
+
0x26: 'ampersand',
|
|
375
|
+
0x27: 'apostrophe',
|
|
376
|
+
0x28: 'parenleft',
|
|
377
|
+
0x29: 'parenright',
|
|
378
|
+
0x2a: 'asterisk',
|
|
379
|
+
0x2b: 'plus',
|
|
380
|
+
0x2c: 'comma',
|
|
381
|
+
0x2d: 'minus',
|
|
382
|
+
0x2e: 'period',
|
|
383
|
+
0x2f: 'slash',
|
|
384
|
+
0x3a: 'colon',
|
|
385
|
+
0x3b: 'semicolon',
|
|
386
|
+
0x3c: 'less',
|
|
387
|
+
0x3d: 'equal',
|
|
388
|
+
0x3e: 'greater',
|
|
389
|
+
0x3f: 'question',
|
|
390
|
+
0x40: 'at',
|
|
391
|
+
0x5b: 'bracketleft',
|
|
392
|
+
0x5c: 'backslash',
|
|
393
|
+
0x5d: 'bracketright',
|
|
394
|
+
0x5e: 'asciicircum',
|
|
395
|
+
0x5f: 'underscore',
|
|
396
|
+
0x60: 'grave',
|
|
397
|
+
0x7b: 'braceleft',
|
|
398
|
+
0x7c: 'bar',
|
|
399
|
+
0x7d: 'braceright',
|
|
400
|
+
0x7e: 'asciitilde',
|
|
401
|
+
};
|
|
402
|
+
for (const [code, name] of Object.entries(punctuation)) {
|
|
403
|
+
names.set(name, Number(code));
|
|
404
|
+
}
|
|
405
|
+
for (let c = 0x30; c <= 0x39; c++) names.set(String.fromCharCode(c), c);
|
|
406
|
+
for (let c = 0x41; c <= 0x5a; c++) names.set(String.fromCharCode(c), c);
|
|
407
|
+
for (let c = 0x61; c <= 0x7a; c++) names.set(String.fromCharCode(c), c);
|
|
408
|
+
return names;
|
|
409
|
+
})();
|
|
410
|
+
|
|
411
|
+
const DEAD_NAMES = new Map([
|
|
412
|
+
['Multi_key', XK_MULTI_KEY],
|
|
413
|
+
['dead_grave', XK_DEAD_GRAVE],
|
|
414
|
+
['dead_acute', XK_DEAD_ACUTE],
|
|
415
|
+
['dead_circumflex', XK_DEAD_CIRCUMFLEX],
|
|
416
|
+
['dead_tilde', XK_DEAD_TILDE],
|
|
417
|
+
['dead_macron', XK_DEAD_MACRON],
|
|
418
|
+
['dead_breve', XK_DEAD_BREVE],
|
|
419
|
+
['dead_abovedot', XK_DEAD_ABOVEDOT],
|
|
420
|
+
['dead_diaeresis', XK_DEAD_DIAERESIS],
|
|
421
|
+
['dead_abovering', XK_DEAD_ABOVERING],
|
|
422
|
+
['dead_doubleacute', XK_DEAD_DOUBLEACUTE],
|
|
423
|
+
['dead_caron', XK_DEAD_CARON],
|
|
424
|
+
['dead_cedilla', XK_DEAD_CEDILLA],
|
|
425
|
+
['dead_ogonek', XK_DEAD_OGONEK],
|
|
426
|
+
['dead_iota', XK_DEAD_IOTA],
|
|
427
|
+
['dead_voiced_sound', XK_DEAD_VOICED_SOUND],
|
|
428
|
+
['dead_semivoiced_sound', XK_DEAD_SEMIVOICED_SOUND],
|
|
429
|
+
['dead_belowdot', XK_DEAD_BELOWDOT],
|
|
430
|
+
['dead_hook', XK_DEAD_HOOK],
|
|
431
|
+
['dead_horn', XK_DEAD_HORN],
|
|
432
|
+
['dead_stroke', XK_DEAD_STROKE],
|
|
433
|
+
['dead_abovecomma', XK_DEAD_ABOVECOMMA],
|
|
434
|
+
['dead_psili', XK_DEAD_ABOVECOMMA],
|
|
435
|
+
['dead_abovereversedcomma', XK_DEAD_ABOVEREVERSEDCOMMA],
|
|
436
|
+
['dead_dasia', XK_DEAD_ABOVEREVERSEDCOMMA],
|
|
437
|
+
['dead_doublegrave', XK_DEAD_DOUBLEGRAVE],
|
|
438
|
+
['dead_belowring', XK_DEAD_BELOWRING],
|
|
439
|
+
['dead_belowmacron', XK_DEAD_BELOWMACRON],
|
|
440
|
+
['dead_belowcircumflex', XK_DEAD_BELOWCIRCUMFLEX],
|
|
441
|
+
['dead_belowtilde', XK_DEAD_BELOWTILDE],
|
|
442
|
+
['dead_belowbreve', XK_DEAD_BELOWBREVE],
|
|
443
|
+
['dead_belowdiaeresis', XK_DEAD_BELOWDIAERESIS],
|
|
444
|
+
['dead_invertedbreve', XK_DEAD_INVERTEDBREVE],
|
|
445
|
+
['dead_belowcomma', XK_DEAD_BELOWCOMMA],
|
|
446
|
+
['dead_currency', XK_DEAD_CURRENCY],
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
/** `<name>` → keysym, or undefined for a name outside what we can resolve. */
|
|
450
|
+
function keysymNamed(name) {
|
|
451
|
+
const dead = DEAD_NAMES.get(name);
|
|
452
|
+
if (dead !== undefined) return dead;
|
|
453
|
+
const ascii = ASCII_NAMES.get(name);
|
|
454
|
+
if (ascii !== undefined) return ascii;
|
|
455
|
+
// the two escape hatches X's own format has for everything unnamed
|
|
456
|
+
if (/^U[0-9A-Fa-f]{4,6}$/.test(name)) {
|
|
457
|
+
return keysymOf(String.fromCodePoint(parseInt(name.slice(1), 16)));
|
|
458
|
+
}
|
|
459
|
+
if (/^0x[0-9A-Fa-f]+$/.test(name)) return Number(name);
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const LINE =
|
|
464
|
+
/^\s*((?:<[A-Za-z_0-9]+>\s*)+):\s*(?:"((?:[^"\\]|\\.)*)"|([A-Za-z_0-9]+))/;
|
|
465
|
+
|
|
466
|
+
function unescape(text) {
|
|
467
|
+
return text.replace(/\\(x[0-9A-Fa-f]{2}|[0-7]{1,3}|.)/g, (_, esc) => {
|
|
468
|
+
if (esc[0] === 'x') return String.fromCharCode(parseInt(esc.slice(1), 16));
|
|
469
|
+
if (/^[0-7]+$/.test(esc)) return String.fromCharCode(parseInt(esc, 8));
|
|
470
|
+
return { n: '\n', t: '\t', r: '\r' }[esc] ?? esc;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Parse X's Compose format — the one in `/usr/share/X11/locale/<locale>/Compose`
|
|
476
|
+
* and in `~/.XCompose`:
|
|
477
|
+
*
|
|
478
|
+
* ```
|
|
479
|
+
* <Multi_key> <o> <c> : "©" copyright
|
|
480
|
+
* <dead_acute> <e> : "é" eacute
|
|
481
|
+
* ```
|
|
482
|
+
*
|
|
483
|
+
* Returns the sequences it understood and a count of the lines it did not.
|
|
484
|
+
* Two things it does not do, both stated rather than silently absorbed:
|
|
485
|
+
* `include` directives are ignored (the system file's first line includes
|
|
486
|
+
* the locale's, so a bare `~/.XCompose` may parse to almost nothing), and a
|
|
487
|
+
* line whose input side names a keysym outside ASCII and the dead-key block
|
|
488
|
+
* — `<Greek_alpha>`, `<Cyrillic_a>` — is skipped, because resolving those
|
|
489
|
+
* names needs a table this package does not carry.
|
|
490
|
+
*/
|
|
491
|
+
export function parseCompose(text) {
|
|
492
|
+
const sequences = [];
|
|
493
|
+
let skipped = 0;
|
|
494
|
+
for (const line of String(text).split('\n')) {
|
|
495
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
496
|
+
const match = LINE.exec(line);
|
|
497
|
+
if (!match) {
|
|
498
|
+
if (!/^\s*include\b/.test(line)) skipped++;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const [, lhs, quoted, named] = match;
|
|
502
|
+
const keysyms = [];
|
|
503
|
+
let ok = true;
|
|
504
|
+
for (const [, name] of lhs.matchAll(/<([A-Za-z_0-9]+)>/g)) {
|
|
505
|
+
const keysym = keysymNamed(name);
|
|
506
|
+
if (keysym === undefined) {
|
|
507
|
+
ok = false;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
keysyms.push(keysym);
|
|
511
|
+
}
|
|
512
|
+
// the right-hand side may be a string, a keysym name, or both; the
|
|
513
|
+
// string wins, and a bare name is the character that keysym types
|
|
514
|
+
const result =
|
|
515
|
+
quoted !== undefined ? unescape(quoted) : charOf(keysymNamed(named) ?? 0);
|
|
516
|
+
if (!ok || !result) {
|
|
517
|
+
skipped++;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
sequences.push([keysyms, result]);
|
|
521
|
+
}
|
|
522
|
+
return { sequences, skipped };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The Compose file this machine would use, or `null` when there is none —
|
|
527
|
+
* which is the normal answer on macOS, where XQuartz ships no locale tree.
|
|
528
|
+
*
|
|
529
|
+
* The search is Xlib's: `$XCOMPOSEFILE`, then a personal `~/.XCompose`,
|
|
530
|
+
* then the locale's file under the X11 tree.
|
|
531
|
+
*/
|
|
532
|
+
export function systemComposeFile() {
|
|
533
|
+
const candidates = [];
|
|
534
|
+
if (process.env.XCOMPOSEFILE) candidates.push(process.env.XCOMPOSEFILE);
|
|
535
|
+
const home = process.env.HOME || homedir();
|
|
536
|
+
if (home) candidates.push(join(home, '.XCompose'));
|
|
537
|
+
const locale =
|
|
538
|
+
process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || '';
|
|
539
|
+
const name = locale.split(':')[0] || 'en_US.UTF-8';
|
|
540
|
+
for (const dir of ['/usr/share/X11/locale', '/opt/X11/share/X11/locale']) {
|
|
541
|
+
candidates.push(join(dir, name, 'Compose'));
|
|
542
|
+
candidates.push(join(dir, 'en_US.UTF-8', 'Compose'));
|
|
543
|
+
}
|
|
544
|
+
for (const path of candidates) {
|
|
545
|
+
try {
|
|
546
|
+
readFileSync(path);
|
|
547
|
+
return path;
|
|
548
|
+
} catch {
|
|
549
|
+
// not there, or not readable: try the next rung
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Build the table a root composes with.
|
|
557
|
+
*
|
|
558
|
+
* `option` is `createRoot`'s `compose`:
|
|
559
|
+
* `undefined` — the built-ins; `false` — no composition at all;
|
|
560
|
+
* `'system'` — the built-ins plus this machine's Compose file;
|
|
561
|
+
* `{ file, sequences }` — the built-ins plus a file and/or explicit
|
|
562
|
+
* sequences, each overriding what came before it.
|
|
563
|
+
*/
|
|
564
|
+
export function composeTable(option) {
|
|
565
|
+
if (option === false) return null;
|
|
566
|
+
const settings =
|
|
567
|
+
option === 'system'
|
|
568
|
+
? { file: 'system' }
|
|
569
|
+
: typeof option === 'object' && option !== null
|
|
570
|
+
? option
|
|
571
|
+
: {};
|
|
572
|
+
const base = builtinCompose();
|
|
573
|
+
if (!settings.file && !settings.sequences) return base;
|
|
574
|
+
|
|
575
|
+
// a copy, so one root's Compose file is not every root's
|
|
576
|
+
const extended = new ComposeTable();
|
|
577
|
+
copyInto(extended, base.root, []);
|
|
578
|
+
|
|
579
|
+
if (settings.file) {
|
|
580
|
+
const path =
|
|
581
|
+
settings.file === 'system' ? systemComposeFile() : settings.file;
|
|
582
|
+
if (path) {
|
|
583
|
+
let text = null;
|
|
584
|
+
try {
|
|
585
|
+
text = readFileSync(path, 'utf8');
|
|
586
|
+
} catch (err) {
|
|
587
|
+
// Named explicitly and not there is a mistake worth hearing about;
|
|
588
|
+
// 'system' finding nothing is Tuesday on macOS and says nothing.
|
|
589
|
+
if (settings.file !== 'system') {
|
|
590
|
+
console.warn(
|
|
591
|
+
`react-x11: could not read the Compose file ${path} ` +
|
|
592
|
+
`(${err?.code ?? err?.message}). Composition falls back to the ` +
|
|
593
|
+
'built-in sequences; pass compose: false to turn it off.',
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (text) {
|
|
598
|
+
for (const [keysyms, result] of parseCompose(text).sequences) {
|
|
599
|
+
extended.add(keysyms, result);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (const [keysyms, result] of settings.sequences ?? []) {
|
|
605
|
+
extended.add(
|
|
606
|
+
Array.from(keysyms, (k) => (typeof k === 'string' ? keysymOf(k) : k)),
|
|
607
|
+
result,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return extended;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function copyInto(table, node, path) {
|
|
614
|
+
if (node.text !== undefined) table.add(path, node.text);
|
|
615
|
+
for (const [keysym, child] of node.children) {
|
|
616
|
+
copyInto(table, child, [...path, keysym]);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Install a root's table on its connection, the way the other per-root
|
|
622
|
+
* settings ride the app object. Returns it so a caller can tell composition
|
|
623
|
+
* off from composition on.
|
|
624
|
+
*/
|
|
625
|
+
export function beginCompose(app, option) {
|
|
626
|
+
const table = composeTable(option);
|
|
627
|
+
if (app) app._reactX11Compose = table;
|
|
628
|
+
return table;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** The table a window composes with: its root's, or the built-ins for an
|
|
632
|
+
* app that never went through `createRoot` (a unit test, a mock). */
|
|
633
|
+
export function composeTableFor(app) {
|
|
634
|
+
const table = app?._reactX11Compose;
|
|
635
|
+
return table === undefined ? builtinCompose() : table;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// --- the machine -----------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
// Modifier keysyms, plus the two group/level switches. A sequence has to
|
|
641
|
+
// survive them: reaching an uppercase letter means pressing Shift, and
|
|
642
|
+
// AltGr is how half of Europe reaches a dead key in the first place.
|
|
643
|
+
const MODIFIER_LOW = 0xffe1;
|
|
644
|
+
const MODIFIER_HIGH = 0xffee;
|
|
645
|
+
const XK_MODE_SWITCH = 0xff7e;
|
|
646
|
+
const XK_ISO_LEVEL3_SHIFT = 0xfe03;
|
|
647
|
+
const XK_ISO_LEVEL5_SHIFT = 0xfe11;
|
|
648
|
+
|
|
649
|
+
function isModifier(keysym) {
|
|
650
|
+
return (
|
|
651
|
+
(keysym >= MODIFIER_LOW && keysym <= MODIFIER_HIGH) ||
|
|
652
|
+
keysym === XK_MODE_SWITCH ||
|
|
653
|
+
keysym === XK_ISO_LEVEL3_SHIFT ||
|
|
654
|
+
keysym === XK_ISO_LEVEL5_SHIFT
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** The character a pending keysym stands for. `Multi_key` is the one that
|
|
659
|
+
* differs between the two readers: it *shows* as a mark, and it *types*
|
|
660
|
+
* nothing, because `·` is a note about the keyboard rather than something
|
|
661
|
+
* anybody pressed a key to say. */
|
|
662
|
+
function markOf(keysym) {
|
|
663
|
+
return SPACING[keysym] ?? MARKS[keysym] ?? charOf(keysym);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** What an open sequence shows in the preedit. */
|
|
667
|
+
function preeditOf(keys) {
|
|
668
|
+
return keys
|
|
669
|
+
.map((k) => (k === XK_MULTI_KEY ? COMPOSE_MARK : markOf(k)))
|
|
670
|
+
.join('');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** What an open sequence types when it turns out not to be one. */
|
|
674
|
+
function typedOf(keys) {
|
|
675
|
+
return keys.map((k) => (k === XK_MULTI_KEY ? '' : markOf(k))).join('');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** The dead key a pending keysym stands for, if any — a `dead_*` keysym is
|
|
679
|
+
* itself, and a character after `Multi_key` may name one. */
|
|
680
|
+
function deadOf(keysym, viaMulti) {
|
|
681
|
+
if (isDeadKeysym(keysym)) return keysym;
|
|
682
|
+
if (!viaMulti) return undefined;
|
|
683
|
+
return ACCENTS[charOf(keysym)];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Apply dead keys to a base keysym, or undefined if they do not compose. */
|
|
687
|
+
function applyDead(deads, base) {
|
|
688
|
+
if (base === XK_SPACE) {
|
|
689
|
+
// the accent on its own, which is how `´` is typed at all on a layout
|
|
690
|
+
// whose only acute is a dead key
|
|
691
|
+
return deads.map((d) => SPACING[d] ?? MARKS[d] ?? '').join('');
|
|
692
|
+
}
|
|
693
|
+
const char = charOf(base);
|
|
694
|
+
if (!char) return undefined;
|
|
695
|
+
if (deads.length === 1) {
|
|
696
|
+
const named = UNCOMPOSED[deads[0]]?.[char];
|
|
697
|
+
if (named) return named;
|
|
698
|
+
}
|
|
699
|
+
let text = char;
|
|
700
|
+
for (const dead of deads) {
|
|
701
|
+
const mark = MARKS[dead];
|
|
702
|
+
if (!mark) return undefined;
|
|
703
|
+
text += mark;
|
|
704
|
+
}
|
|
705
|
+
const composed = text.normalize('NFC');
|
|
706
|
+
// A sequence that stays decomposed is one Unicode has no character for:
|
|
707
|
+
// `dead_acute` + `q` normalises to `q` + U+0301 and is not a `q́` anybody
|
|
708
|
+
// meant to type. Two code points out means no.
|
|
709
|
+
return Array.from(composed).length === 1 ? composed : undefined;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Everything the algorithmic path can make of a sequence — the dead-key
|
|
714
|
+
* table, in both the orders the Compose file writes them.
|
|
715
|
+
*/
|
|
716
|
+
function derive(keys) {
|
|
717
|
+
const viaMulti = keys[0] === XK_MULTI_KEY;
|
|
718
|
+
const body = viaMulti ? keys.slice(1) : keys;
|
|
719
|
+
if (body.length < 2) return undefined;
|
|
720
|
+
const direct = inOrder(body, viaMulti);
|
|
721
|
+
if (direct !== undefined) return direct;
|
|
722
|
+
// `Compose e '` as well as `Compose ' e`: the standard file spells the
|
|
723
|
+
// common ones both ways round, and a user who learned one is not going to
|
|
724
|
+
// be told the other is the real one.
|
|
725
|
+
if (viaMulti && body.length === 2) return inOrder([body[1], body[0]], true);
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Every key but the last read as a dead key, applied to the last. */
|
|
730
|
+
function inOrder(body, viaMulti) {
|
|
731
|
+
const deads = [];
|
|
732
|
+
for (const keysym of body.slice(0, -1)) {
|
|
733
|
+
const dead = deadOf(keysym, viaMulti);
|
|
734
|
+
if (dead === undefined) return undefined;
|
|
735
|
+
deads.push(dead);
|
|
736
|
+
}
|
|
737
|
+
return applyDead(deads, body[body.length - 1]);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* The composition state machine for one keyboard focus.
|
|
742
|
+
*
|
|
743
|
+
* `probe` is pure and `apply` is what commits the transition, deliberately:
|
|
744
|
+
* the key event is dispatched to the application *before* the composer eats
|
|
745
|
+
* it, so an `onKeyDown` that calls `preventDefault()` gets to keep its
|
|
746
|
+
* chord — and it can only do that if asking what would happen has not
|
|
747
|
+
* already changed anything.
|
|
748
|
+
*/
|
|
749
|
+
export class Composer {
|
|
750
|
+
constructor(table = builtinCompose()) {
|
|
751
|
+
this.table = table;
|
|
752
|
+
this.keys = [];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
get composing() {
|
|
756
|
+
return this.keys.length > 0;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
get preedit() {
|
|
760
|
+
return preeditOf(this.keys);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
reset() {
|
|
764
|
+
this.keys = [];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* What this keysym would do, without doing it.
|
|
769
|
+
*
|
|
770
|
+
* @returns {{keys: number[], consumed: boolean,
|
|
771
|
+
* preedit: string|null, text: string|null}}
|
|
772
|
+
* `consumed` is whether the key belongs to the composition rather than
|
|
773
|
+
* to the application, `preedit` is what to show (null for "this key is
|
|
774
|
+
* nothing to do with composition"), and `text` is what to insert.
|
|
775
|
+
*/
|
|
776
|
+
probe(keysym) {
|
|
777
|
+
const idle = {
|
|
778
|
+
keys: this.keys,
|
|
779
|
+
consumed: false,
|
|
780
|
+
preedit: null,
|
|
781
|
+
text: null,
|
|
782
|
+
};
|
|
783
|
+
if (keysym == null || isModifier(keysym)) return idle;
|
|
784
|
+
|
|
785
|
+
if (!this.composing) {
|
|
786
|
+
if (keysym === XK_MULTI_KEY || isDeadKeysym(keysym)) {
|
|
787
|
+
return this._pending([keysym]);
|
|
788
|
+
}
|
|
789
|
+
return idle;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Escape abandons the sequence: nothing typed, and the accent that was
|
|
793
|
+
// showing goes away. The one gesture every composing user needs and the
|
|
794
|
+
// only one that cannot be a fallback, since Escape has no character.
|
|
795
|
+
if (keysym === XK_ESCAPE) {
|
|
796
|
+
return { keys: [], consumed: true, preedit: '', text: null };
|
|
797
|
+
}
|
|
798
|
+
// Backspace un-presses the last key of the sequence, the way it undoes
|
|
799
|
+
// anything else half-typed.
|
|
800
|
+
if (keysym === XK_BACKSPACE) {
|
|
801
|
+
return this._pending(this.keys.slice(0, -1));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const candidate = [...this.keys, keysym];
|
|
805
|
+
const hit = this.table.lookup(candidate);
|
|
806
|
+
if (hit.text !== undefined) return this._commit(hit.text);
|
|
807
|
+
const derived = derive(candidate);
|
|
808
|
+
// A table entry that is also a prefix (`Compose - -` before
|
|
809
|
+
// `Compose - - -`) commits the shorter one, because there is nothing to
|
|
810
|
+
// wait for: X's own tables do not nest that way.
|
|
811
|
+
if (derived !== undefined) return this._commit(derived);
|
|
812
|
+
if (hit.prefix) return this._pending(candidate);
|
|
813
|
+
// A dead key on a dead key stacks — Vietnamese `ế` is circumflex then
|
|
814
|
+
// acute then `e` — except for the same one twice, which every layout
|
|
815
|
+
// uses to type the accent itself.
|
|
816
|
+
if (isDeadKeysym(keysym)) {
|
|
817
|
+
if (keysym === this.keys[this.keys.length - 1]) {
|
|
818
|
+
return this._commit(markOf(keysym));
|
|
819
|
+
}
|
|
820
|
+
return this._pending(candidate);
|
|
821
|
+
}
|
|
822
|
+
// The key straight after Compose always waits for one more, whatever the
|
|
823
|
+
// table says. That is what makes `Compose e '` work as well as
|
|
824
|
+
// `Compose ' e`: the standard file spells the common accents both ways
|
|
825
|
+
// round, and the letter-first order cannot be recognised until the
|
|
826
|
+
// accent after it has arrived.
|
|
827
|
+
if (
|
|
828
|
+
this.keys.length === 1 &&
|
|
829
|
+
this.keys[0] === XK_MULTI_KEY &&
|
|
830
|
+
charOf(keysym)
|
|
831
|
+
) {
|
|
832
|
+
return this._pending(candidate);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// No sequence, so the keys are typed as they came: X's rule for a failed
|
|
836
|
+
// composition is that nothing is swallowed. A key with no character of
|
|
837
|
+
// its own — an arrow, Return — hands the accent over and then takes its
|
|
838
|
+
// normal turn, which is why `consumed` is false while `text` is not null.
|
|
839
|
+
const accent = typedOf(this.keys);
|
|
840
|
+
const char = charOf(keysym);
|
|
841
|
+
if (!char) {
|
|
842
|
+
return { keys: [], consumed: false, preedit: '', text: accent };
|
|
843
|
+
}
|
|
844
|
+
return this._commit(accent + char);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
_pending(keys) {
|
|
848
|
+
if (keys.length === 0) {
|
|
849
|
+
return { keys, consumed: true, preedit: '', text: null };
|
|
850
|
+
}
|
|
851
|
+
return { keys, consumed: true, preedit: preeditOf(keys), text: null };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
_commit(text) {
|
|
855
|
+
return { keys: [], consumed: true, preedit: '', text };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Commit a `probe` result. */
|
|
859
|
+
apply(result) {
|
|
860
|
+
this.keys = result.keys;
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** Probe and apply in one step — what a test or a non-event caller wants. */
|
|
865
|
+
feed(keysym) {
|
|
866
|
+
return this.apply(this.probe(keysym));
|
|
867
|
+
}
|
|
868
|
+
}
|