Compare commits
539
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4997f529 | ||
|
|
16ea2ec0ef | ||
|
|
eb0f5383c3 | ||
|
|
a4502a6d00 | ||
|
|
c11a8f43f4 | ||
|
|
d7ba56cd28 | ||
|
|
be44833f12 | ||
|
|
fcb3cc1a21 | ||
|
|
376268157c | ||
|
|
5ceb02b9c6 | ||
|
|
d320898231 | ||
|
|
cf3939af4a | ||
|
|
596ab845d0 | ||
|
|
a59d527d1a | ||
|
|
08f85cf826 | ||
|
|
e95ec8d4bc | ||
|
|
d15b426bd4 | ||
|
|
c907edd75f | ||
|
|
1029e19b57 | ||
|
|
d44900faa2 | ||
|
|
5f724c5924 | ||
|
|
ae58176bcb | ||
|
|
429b2882f1 | ||
|
|
ffac8c03ab | ||
|
|
a7c4a160a8 | ||
|
|
c9dc994090 | ||
|
|
e4710d3286 | ||
|
|
8f9045ed88 | ||
|
|
b8e5ec7304 | ||
|
|
6a3d2480e0 | ||
|
|
735afde77f | ||
|
|
53efc95170 | ||
|
|
3512f140de | ||
|
|
dbe35428cb | ||
|
|
297ac838a3 | ||
|
|
5316cb0812 | ||
|
|
c124550971 | ||
|
|
6525a7e68e | ||
|
|
b2133d93f0 | ||
|
|
a5e4f90139 | ||
|
|
a27b8feb8f | ||
|
|
e96748cc79 | ||
|
|
334c8170fe | ||
|
|
2bb9662bea | ||
|
|
7cba3c9fa1 | ||
|
|
bab7db13db | ||
|
|
1c79c5511c | ||
|
|
7b417f4585 | ||
|
|
dc487dfd6f | ||
|
|
36baf70bea | ||
|
|
87e0b9ab10 | ||
|
|
5e532c13c6 | ||
|
|
db131e0c22 | ||
|
|
51b9a713da | ||
|
|
972c069fa3 | ||
|
|
2c0ba3c058 | ||
|
|
e515647f99 | ||
|
|
7a2794a552 | ||
|
|
da2267a10f | ||
|
|
34e68b1ad4 | ||
|
|
1c42957461 | ||
|
|
77036d3f12 | ||
|
|
ccbf612ddb | ||
|
|
eed8423030 | ||
|
|
431d511c95 | ||
|
|
0ca1bd48be | ||
|
|
7ceb15bb83 | ||
|
|
5ecdc7da44 | ||
|
|
2e2270d01f | ||
|
|
042bb4bed9 | ||
|
|
7e3c41ba49 | ||
|
|
21e4301e38 | ||
|
|
3ef43aedb1 | ||
|
|
9eaf5b45dc | ||
|
|
39037ee806 | ||
|
|
e542affaca | ||
|
|
038883c41b | ||
|
|
6c3d4e4ea4 | ||
|
|
659cfddd8c | ||
|
|
c375696ee6 | ||
|
|
bc3c376e83 | ||
|
|
9fca3d8ecf | ||
|
|
8eed7b4163 | ||
|
|
c29938ee83 | ||
|
|
0e679ea6ae | ||
|
|
d0e8141f7c | ||
|
|
1d322c9e45 | ||
|
|
bad7e329df | ||
|
|
ae5e7e55c8 | ||
|
|
5bb2e46508 | ||
|
|
5938b8823d | ||
|
|
69d87c8a30 | ||
|
|
a247154650 | ||
|
|
b30d9c3533 | ||
|
|
60190c1c48 | ||
|
|
99d69d2236 | ||
|
|
de01d4608c | ||
|
|
d35481c3f9 | ||
|
|
e2d84928b7 | ||
|
|
9ffd16aa9b | ||
|
|
77fa03d11c | ||
|
|
01450f044a | ||
|
|
b04d816d8a | ||
|
|
f80dbf3d03 | ||
|
|
b58bd452af | ||
|
|
e8d99d50a6 | ||
|
|
bae314f67c | ||
|
|
cb8159c4f2 | ||
|
|
b87cba4c2a | ||
|
|
492d950b64 | ||
|
|
a1b69e5ac8 | ||
|
|
d77711936f | ||
|
|
5a79b08661 | ||
|
|
3e8fc48b33 | ||
|
|
8c9afdb364 | ||
|
|
f89524e577 | ||
|
|
a1c1702508 | ||
|
|
15c71e28c8 | ||
|
|
fd641370ec | ||
|
|
259cc67f12 | ||
|
|
b0b0868e90 | ||
|
|
d58e8597cd | ||
|
|
17d439515f | ||
|
|
ae171acb79 | ||
|
|
e2dabc77fd | ||
|
|
40342ecd1e | ||
|
|
1442f84e4e | ||
|
|
6f87ae77a7 | ||
|
|
5f25e05cd2 | ||
|
|
2af9d10171 | ||
|
|
f471c0add8 | ||
|
|
62dc0d9c51 | ||
|
|
5e95ef652d | ||
|
|
14525b334f | ||
|
|
bc1d87dade | ||
|
|
849110d300 | ||
|
|
8b6f8f3172 | ||
|
|
27825f9ed8 | ||
|
|
a507afb388 | ||
|
|
ec5b421dfb | ||
|
|
8f0e711a79 | ||
|
|
fc0dc3fb07 | ||
|
|
bea1628015 | ||
|
|
447746314e | ||
|
|
6a02d7c9f5 | ||
|
|
1ace108422 | ||
|
|
2c1441a52d | ||
|
|
304943658d | ||
|
|
8e67c0093c | ||
|
|
ee3b3626e8 | ||
|
|
b286f21f64 | ||
|
|
3d92a7e2ed | ||
|
|
b01ba7db28 | ||
|
|
1c73006e39 | ||
|
|
03b015d5d3 | ||
|
|
42d408a9a7 | ||
|
|
5ecd980a83 | ||
|
|
63ebe2df5b | ||
|
|
1b2d361b59 | ||
|
|
8e29591b9f | ||
|
|
48df6117ed | ||
|
|
14b6a71391 | ||
|
|
c24eb60bd3 | ||
|
|
2521bab72f | ||
|
|
0f1c9a47db | ||
|
|
777a46445a | ||
|
|
a20ff565b0 | ||
|
|
d28046ebed | ||
|
|
4ddd982e42 | ||
|
|
a3228bd1b8 | ||
|
|
a1fca4feb8 | ||
|
|
afc6f0b7eb | ||
|
|
f4c77d37b4 | ||
|
|
9d0beb7c38 | ||
|
|
f88650ec1c | ||
|
|
8b35c67dd3 | ||
|
|
53dc85eca2 | ||
|
|
0946386cc4 | ||
|
|
ebe18b3da5 | ||
|
|
6c2792a1d5 | ||
|
|
ee47054678 | ||
|
|
91c4761b82 | ||
|
|
c9fc6fca8e | ||
|
|
73cba6dda3 | ||
|
|
50b0f3d7ee | ||
|
|
686c67a14b | ||
|
|
ec78500f86 | ||
|
|
98ea14824a | ||
|
|
4c905dad33 | ||
|
|
e3fdcc7869 | ||
|
|
0674663a5a | ||
|
|
e07832a420 | ||
|
|
934b5dc8fa | ||
|
|
7a544d8346 | ||
|
|
c624a47780 | ||
|
|
5a29d489c3 | ||
|
|
4a67836a5b | ||
|
|
3adf6be988 | ||
|
|
566589f7c1 | ||
|
|
8dbc91dc24 | ||
|
|
992da3cc4d | ||
|
|
3fbaef06ef | ||
|
|
2265fa2ade | ||
|
|
756c353237 | ||
|
|
49a8ac8fc0 | ||
|
|
bf7c95b6cc | ||
|
|
8112fbf210 | ||
|
|
16a13f8504 | ||
|
|
9bd128b315 | ||
|
|
729a670043 | ||
|
|
bd8454db55 | ||
|
|
d6f6a6af9a | ||
|
|
d65f9312ea | ||
|
|
a437d12004 | ||
|
|
437490308c | ||
|
|
3250c31202 | ||
|
|
55d12e03d1 | ||
|
|
6efb319ece | ||
|
|
b69afe5765 | ||
|
|
613aa6ba23 | ||
|
|
a8c9128841 | ||
|
|
94a264679e | ||
|
|
cb35d1b487 | ||
|
|
c97abced76 | ||
|
|
9db31db886 | ||
|
|
ccf9727b85 | ||
|
|
1dfdde9e27 | ||
|
|
2712e88d0a | ||
|
|
b5da8a28f6 | ||
|
|
66c4f8b116 | ||
|
|
9148c80d1a | ||
|
|
e1bfc7dd1f | ||
|
|
c6c4a2ffbc | ||
|
|
2cf2929869 | ||
|
|
b0145998c5 | ||
|
|
0e57643f16 | ||
|
|
a35223a819 | ||
|
|
5e95dc1f10 | ||
|
|
b4e9a7f9ea | ||
|
|
73e3357526 | ||
|
|
483c7b4a81 | ||
|
|
f4453b3e18 | ||
|
|
7389a1097d | ||
|
|
0c7b1cb448 | ||
|
|
7a3b5339e7 | ||
|
|
8193ceae12 | ||
|
|
064e299bb0 | ||
|
|
6211bf078c | ||
|
|
a639808196 | ||
|
|
b522e255f0 | ||
|
|
d7850af9a2 | ||
|
|
4182f2226f | ||
|
|
e2f45b5f4e | ||
|
|
8712f8067d | ||
|
|
08c49301b7 | ||
|
|
917a8ded92 | ||
|
|
d06d278f0c | ||
|
|
f84384c83a | ||
|
|
ec03109c39 | ||
|
|
21b8f77afb | ||
|
|
363fc84fd2 | ||
|
|
8e4de23c11 | ||
|
|
1caf6dbe50 | ||
|
|
1c97e40a6a | ||
|
|
cdbafb231d | ||
|
|
44a8c7efbc | ||
|
|
6277bb363c | ||
|
|
40f2eefec7 | ||
|
|
519b1c5ef7 | ||
|
|
97e356fb83 | ||
|
|
42872a84dd | ||
|
|
4700e18833 | ||
|
|
d143340b60 | ||
|
|
a8f26bb758 | ||
|
|
da4243cf02 | ||
|
|
54e66f3ac4 | ||
|
|
7d8ef60860 | ||
|
|
595c119404 | ||
|
|
481431fbc1 | ||
|
|
5c2cd474ce | ||
|
|
0b9a7cb856 | ||
|
|
a8a4455189 | ||
|
|
11fbdb3db7 | ||
|
|
88ce0d40e7 | ||
|
|
2785f11860 | ||
|
|
857c4529cd | ||
|
|
d30c3e7204 | ||
|
|
982f5e15ff | ||
|
|
ec3e4f2af1 | ||
|
|
13914ee01e | ||
|
|
e27cc2c089 | ||
|
|
214289aaab | ||
|
|
a5af434e3e | ||
|
|
a513120348 | ||
|
|
ef978b370b | ||
|
|
00ad3eac40 | ||
|
|
29d3eb2e52 | ||
|
|
8b594140e9 | ||
|
|
7cf48c1eb5 | ||
|
|
8e6b6e7464 | ||
|
|
1b0554714b | ||
|
|
abdfa2628c | ||
|
|
5b4d794f7a | ||
|
|
9eb1da79e3 | ||
|
|
d4ba4c109b | ||
|
|
1ad23d6081 | ||
|
|
73c01cae89 | ||
|
|
5174a7310f | ||
|
|
1d793c9054 | ||
|
|
1b7c61a6fc | ||
|
|
9780b44115 | ||
|
|
8d532a35c4 | ||
|
|
d6df8d7902 | ||
|
|
4b9566cef0 | ||
|
|
b30bb1e3b0 | ||
|
|
d67e73a4da | ||
|
|
e7434a932d | ||
|
|
cdb0497d0d | ||
|
|
2fb42c554e | ||
|
|
3632496c94 | ||
|
|
a6407fb3c6 | ||
|
|
65aa28464c | ||
|
|
2baad39ccf | ||
|
|
c975956f2a | ||
|
|
4619a755b1 | ||
|
|
bc795d36c0 | ||
|
|
b989c5a568 | ||
|
|
d66214b25e | ||
|
|
6157f523a8 | ||
|
|
3ab93ea083 | ||
|
|
7ef4c55ca9 | ||
|
|
d393154271 | ||
|
|
2c32ef5963 | ||
|
|
a730533790 | ||
|
|
8a5ffc1bb1 | ||
|
|
a966f7d591 | ||
|
|
4985c9f3f9 | ||
|
|
2e1651badd | ||
|
|
7d1f74fb8b | ||
|
|
f8c1f5a32f | ||
|
|
79944f7524 | ||
|
|
52b6710b19 | ||
|
|
2a283a307c | ||
|
|
ab4524873b | ||
|
|
eb5f0d709a | ||
|
|
8a8f262616 | ||
|
|
ca5a681363 | ||
|
|
fb10cd62c9 | ||
|
|
28b62a5e0b | ||
|
|
bedf7e8326 | ||
|
|
f50028abce | ||
|
|
7a4c9cadff | ||
|
|
01c1c477c3 | ||
|
|
33daadb4ea | ||
|
|
ef6013aac9 | ||
|
|
82f24ad5a5 | ||
|
|
97c435862b | ||
|
|
6488e839a1 | ||
|
|
5d3564bb38 | ||
|
|
50985d30f9 | ||
|
|
733e48ec73 | ||
|
|
057fefd39e | ||
|
|
4d48f8624b | ||
|
|
995b491a7a | ||
|
|
4fe118a8ff | ||
|
|
45375c94e2 | ||
|
|
f83760d027 | ||
|
|
1a1bdefc83 | ||
|
|
e28db9e8a9 | ||
|
|
be5f510bfb | ||
|
|
ac6b8ebb10 | ||
|
|
1ada355048 | ||
|
|
4ce3e3106b | ||
|
|
942aff6af7 | ||
|
|
635b898dc0 | ||
|
|
e01d1f2f17 | ||
|
|
bd16eeed90 | ||
|
|
df8f9fc519 | ||
|
|
373bf513f1 | ||
|
|
25a5497329 | ||
|
|
4406043e6b | ||
|
|
82f961f76f | ||
|
|
a26471b9f3 | ||
|
|
ba989a6e6c | ||
|
|
d4f725c649 | ||
|
|
79f5198b2d | ||
|
|
d8a93b757a | ||
|
|
9453634f56 | ||
|
|
10e12cf743 | ||
|
|
79621a4e3f | ||
|
|
546c5e081d | ||
|
|
e6ad59b1b1 | ||
|
|
8eab98b78d | ||
|
|
0ed6fd0db6 | ||
|
|
e3f87ab8a1 | ||
|
|
8119bb0a4a | ||
|
|
dc0d7c81d9 | ||
|
|
fa87ecdc4e | ||
|
|
b82e8f345f | ||
|
|
02e9e56eef | ||
|
|
4fffb96f25 | ||
|
|
6fe5a124ca | ||
|
|
f62043de91 | ||
|
|
55e5ec6037 | ||
|
|
7b2a5cb50f | ||
|
|
8740dcda4d | ||
|
|
c046f5b4b1 | ||
|
|
6bde923f07 | ||
|
|
40b2bbef34 | ||
|
|
c04806fc92 | ||
|
|
8b1306f46d | ||
|
|
6612153c44 | ||
|
|
2be529ce85 | ||
|
|
96aa97ce11 | ||
|
|
98f4c3128c | ||
|
|
ce1ab6fdc4 | ||
|
|
e87e0f5443 | ||
|
|
4a8d8bd247 | ||
|
|
edf100a24b | ||
|
|
ed2d8b8fdc | ||
|
|
faeefb53bf | ||
|
|
32ff1f2115 | ||
|
|
acb59daf33 | ||
|
|
a3d6bf6710 | ||
|
|
5e5c623d8b | ||
|
|
168520f727 | ||
|
|
23f61911e2 | ||
|
|
491fa5586a | ||
|
|
2f549d74ae | ||
|
|
9df2430433 | ||
|
|
ce9f6275ed | ||
|
|
9ca04799fe | ||
|
|
83757d1bf0 | ||
|
|
60fa9122a2 | ||
|
|
a8d40a1edf | ||
|
|
78cc07f88a | ||
|
|
72d7c69a29 | ||
|
|
0e0b445784 | ||
|
|
5d24866042 | ||
|
|
cbea462737 | ||
|
|
a3ae94af1d | ||
|
|
0e35521810 | ||
|
|
40590fedfa | ||
|
|
1551d94de4 | ||
|
|
d605803866 | ||
|
|
a797852dfd | ||
|
|
d42f2b54ff | ||
|
|
79084c3066 | ||
|
|
b4f4946cca | ||
|
|
ed779b9062 | ||
|
|
20fdfba16f | ||
|
|
11dae84ff7 | ||
|
|
579573e77d | ||
|
|
2114abe88c | ||
|
|
d4fd96639f | ||
|
|
269d8c20ae | ||
|
|
49b49627f1 | ||
|
|
663b94011a | ||
|
|
54fcb1ea2f | ||
|
|
a9da3fbf05 | ||
|
|
091c3967f7 | ||
|
|
10d6ce3805 | ||
|
|
eb285af11d | ||
|
|
0976935f87 | ||
|
|
884b667f4e | ||
|
|
b8843732bf | ||
|
|
44d8d2c873 | ||
|
|
3d3abf9cf2 | ||
|
|
268f7af5db | ||
|
|
16b72dfd63 | ||
|
|
a0e658a18c | ||
|
|
ea6385f310 | ||
|
|
02a9d2b047 | ||
|
|
5c0c553936 | ||
|
|
eb743e0e19 | ||
|
|
29d91fe264 | ||
|
|
e8dcf46f39 | ||
|
|
66c1316f87 | ||
|
|
e1803ed5a5 | ||
|
|
1c3813f3d4 | ||
|
|
e12aef9287 | ||
|
|
f5edba0b64 | ||
|
|
5a995e5595 | ||
|
|
2bae212f83 | ||
|
|
bde3680cd1 | ||
|
|
717399ceb6 | ||
|
|
51121f1913 | ||
|
|
483acd3335 | ||
|
|
9dc8cecea3 | ||
|
|
89c2649504 | ||
|
|
390076fc3f | ||
|
|
a4edeed85d | ||
|
|
e61697fc86 | ||
|
|
9c019b44c3 | ||
|
|
5579d83f7d | ||
|
|
429065487a | ||
|
|
219036cb0f | ||
|
|
3e80637145 | ||
|
|
0af884d379 | ||
|
|
5ae9ddf4b8 | ||
|
|
7ac068844c | ||
|
|
5bf3d57d16 | ||
|
|
6461829411 | ||
|
|
efd6a52033 | ||
|
|
3d9e976cb3 | ||
|
|
415de67106 | ||
|
|
c17c01086f | ||
|
|
ce41b175ee | ||
|
|
32b4547c5a | ||
|
|
b64543aa59 | ||
|
|
f049c42acc | ||
|
|
28760cf482 | ||
|
|
4982305b51 | ||
|
|
7fee40ddcd | ||
|
|
180183e876 | ||
|
|
8308ede3f5 | ||
|
|
a6db7b41f6 | ||
|
|
f11241d9de | ||
|
|
937deb9f4b | ||
|
|
6acc861b06 | ||
|
|
8f05b1854b | ||
|
|
0f461e7a3e | ||
|
|
4237d82923 | ||
|
|
187156764a | ||
|
|
d03416467d | ||
|
|
085a275038 | ||
|
|
104c63d7e0 | ||
|
|
70c9861c39 | ||
|
|
c93623cb3b | ||
|
|
b0389a2ed1 | ||
|
|
7907f13774 | ||
|
|
956c7cbcba | ||
|
|
23bef99132 | ||
|
|
b104bcd144 | ||
|
|
809b9aa1ad | ||
|
|
9a536f758d | ||
|
|
15a63bae22 | ||
|
|
41640d9402 | ||
|
|
15664de472 |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:22-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": 18
|
||||
"version": 22
|
||||
} // yarn
|
||||
},
|
||||
"forwardPorts": [
|
||||
|
||||
@@ -5,7 +5,6 @@ indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
|
||||
[*.{j,t}s]
|
||||
max_line_length = 100
|
||||
|
||||
-273
@@ -1,273 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"demo/tsconfig.json",
|
||||
"src/browser/tsconfig.json",
|
||||
"src/common/tsconfig.json",
|
||||
"src/headless/tsconfig.json",
|
||||
"src/vs/tsconfig.json",
|
||||
"test/benchmark/tsconfig.json",
|
||||
"test/playwright/tsconfig.json",
|
||||
"addons/addon-attach/src/tsconfig.json",
|
||||
"addons/addon-attach/test/tsconfig.json",
|
||||
"addons/addon-clipboard/src/tsconfig.json",
|
||||
"addons/addon-clipboard/test/tsconfig.json",
|
||||
"addons/addon-fit/src/tsconfig.json",
|
||||
"addons/addon-fit/test/tsconfig.json",
|
||||
"addons/addon-image/src/tsconfig.json",
|
||||
"addons/addon-image/test/tsconfig.json",
|
||||
"addons/addon-ligatures/src/tsconfig.json",
|
||||
"addons/addon-progress/src/tsconfig.json",
|
||||
"addons/addon-progress/test/tsconfig.json",
|
||||
"addons/addon-search/src/tsconfig.json",
|
||||
"addons/addon-search/test/tsconfig.json",
|
||||
"addons/addon-serialize/src/tsconfig.json",
|
||||
"addons/addon-serialize/test/tsconfig.json",
|
||||
"addons/addon-serialize/benchmark/tsconfig.json",
|
||||
"addons/addon-unicode11/src/tsconfig.json",
|
||||
"addons/addon-unicode11/test/tsconfig.json",
|
||||
"addons/addon-unicode-graphemes/src/tsconfig.json",
|
||||
"addons/addon-unicode-graphemes/test/tsconfig.json",
|
||||
"addons/addon-unicode-graphemes/benchmark/tsconfig.json",
|
||||
"addons/addon-web-links/src/tsconfig.json",
|
||||
"addons/addon-web-links/test/tsconfig.json",
|
||||
"addons/addon-webgl/src/tsconfig.json",
|
||||
"addons/addon-webgl/test/tsconfig.json"
|
||||
],
|
||||
"sourceType": "module"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"addons/*/src/third-party/*.ts",
|
||||
"src/vs/*",
|
||||
"out/*",
|
||||
"out-test/*",
|
||||
"out-esbuild/*",
|
||||
"out-esbuild-test/*",
|
||||
"**/inwasm-sdks/*",
|
||||
"**/typings/*.d.ts",
|
||||
"**/node_modules",
|
||||
"**/*.js",
|
||||
"**/*.mjs"
|
||||
],
|
||||
"plugins": [
|
||||
"@stylistic/ts",
|
||||
"@typescript-eslint",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"@stylistic/ts/indent": [
|
||||
"warn",
|
||||
2
|
||||
],
|
||||
"@stylistic/ts/semi": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"@stylistic/ts/quotes": [
|
||||
"warn",
|
||||
"single",
|
||||
{ "allowTemplateLiterals": true }
|
||||
],
|
||||
|
||||
"@typescript-eslint/array-type": [
|
||||
"warn",
|
||||
{
|
||||
"default": "array",
|
||||
"readonly": "generic"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/consistent-type-assertions": "warn",
|
||||
"@typescript-eslint/consistent-type-definitions": "warn",
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"warn",
|
||||
{
|
||||
"accessibility": "explicit",
|
||||
"overrides": {
|
||||
"constructors": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"warn",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{ "selector": "default", "format": ["camelCase"],
|
||||
"filter": {
|
||||
"regex": "^[a-z]",
|
||||
"match": true
|
||||
}
|
||||
},
|
||||
// variableLike
|
||||
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
|
||||
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
|
||||
// memberLike
|
||||
{ "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
||||
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
||||
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
|
||||
// memberLike - Allow enum-like objects to use UPPER_CASE
|
||||
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"],
|
||||
"filter": {
|
||||
"regex": "^[a-z]",
|
||||
"match": true
|
||||
}
|
||||
},
|
||||
// restrict on* naming for events only
|
||||
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
{ "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
{ "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
// typeLike
|
||||
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
||||
],
|
||||
"@typescript-eslint/no-confusing-void-expression": [
|
||||
"warn",
|
||||
{ "ignoreArrowShorthand": true }
|
||||
],
|
||||
"@typescript-eslint/no-useless-constructor": "warn",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/type-annotation-spacing": "warn",
|
||||
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"objects": "never",
|
||||
"arrays": "never",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"curly": [
|
||||
"warn",
|
||||
"multi-line"
|
||||
],
|
||||
"eol-last": "warn",
|
||||
"eqeqeq": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"jsdoc/check-alignment": 1,
|
||||
"jsdoc/check-param-names": 1,
|
||||
"jsdoc/no-multi-asterisks": 1,
|
||||
"keyword-spacing": "warn",
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 1000, // Don't enforce for code
|
||||
"comments": 100,
|
||||
"ignoreTrailingComments": true,
|
||||
"ignoreUrls": true,
|
||||
"ignorePattern": "^ *((?<vt_comment>(//|\\*) @vt)|(?<table_comment>\\* \\| )|(?<commented_code>// ))"
|
||||
}
|
||||
],
|
||||
"new-parens": "warn",
|
||||
"no-duplicate-imports": "warn",
|
||||
"no-else-return": [
|
||||
"warn",
|
||||
{
|
||||
"allowElseIf": false
|
||||
}
|
||||
],
|
||||
"no-eval": "warn",
|
||||
"no-extra-semi": "error",
|
||||
"no-irregular-whitespace": "warn",
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
"patterns": [
|
||||
".*\\/out\\/.*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "CallExpression[callee.name='requestAnimationFrame']",
|
||||
"message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression[callee.name='cancelAnimationFrame']",
|
||||
"message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']",
|
||||
"message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']",
|
||||
"message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']",
|
||||
"message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService."
|
||||
}
|
||||
],
|
||||
"no-trailing-spaces": "warn",
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"args": "none"
|
||||
}],
|
||||
"no-var": "warn",
|
||||
"one-var": [
|
||||
"warn",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "warn",
|
||||
"spaced-comment": [
|
||||
"warn",
|
||||
"always",
|
||||
{
|
||||
"markers": ["/"],
|
||||
"exceptions": ["-"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.api.ts",
|
||||
"**/*.test.ts"
|
||||
],
|
||||
"rules": {
|
||||
"object-curly-spacing": "off",
|
||||
"max-len": "off",
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@stylistic/ts",
|
||||
"@typescript-eslint",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"@stylistic/ts/indent": [
|
||||
"warn",
|
||||
2
|
||||
],
|
||||
"@stylistic/ts/semi": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"@stylistic/ts/quotes": [
|
||||
"warn",
|
||||
"single",
|
||||
{ "allowTemplateLiterals": true }
|
||||
],
|
||||
|
||||
"@typescript-eslint/array-type": [
|
||||
"warn",
|
||||
{
|
||||
"default": "array",
|
||||
"readonly": "generic"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"warn",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/type-annotation-spacing": "warn",
|
||||
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"objects": "never",
|
||||
"arrays": "never",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"curly": [
|
||||
"warn",
|
||||
"multi-line"
|
||||
],
|
||||
"eol-last": "warn",
|
||||
"eqeqeq": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"jsdoc/check-alignment": 1,
|
||||
"jsdoc/check-param-names": 1,
|
||||
"keyword-spacing": "warn",
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 1000, // Don't enforce for code
|
||||
"comments": 80,
|
||||
"ignoreUrls": true,
|
||||
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
|
||||
}
|
||||
],
|
||||
"no-extra-semi": "error",
|
||||
"no-irregular-whitespace": "warn",
|
||||
"no-trailing-spaces": "warn",
|
||||
"object-curly-spacing": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"spaced-comment": [
|
||||
"warn",
|
||||
"always",
|
||||
{
|
||||
"markers": ["/"],
|
||||
"exceptions": ["-"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,11 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
|
||||
**Testing**:
|
||||
- Unit tests: `npm run test-unit` (Mocha)
|
||||
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
|
||||
- Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js`
|
||||
- Per-addon unit tests: `npm run test-unit -- addons/addon-image/out-esbuild/*.test.js`
|
||||
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
|
||||
- Per-addon integration tests: `npm run test-integration --suite=addon-search`
|
||||
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
|
||||
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
|
||||
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
|
||||
|
||||
## Addon Development Pattern
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
applyTo: '**/*.benchmark.ts'
|
||||
---
|
||||
# Benchmark run instructions
|
||||
|
||||
- Full suite: `npm run benchmark`
|
||||
- Single benchmark file:
|
||||
- Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js`
|
||||
- Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js`
|
||||
- Single context/case:
|
||||
- Use `-t` to get the path, then:
|
||||
- `npm run benchmark -- -s "<path>" out-test/benchmark/Event.benchmark.js`
|
||||
|
||||
When writing instructions, use `RuntimeCase` to measure pure runtime in ms, use `ThroughputRuntimeCase` when measuring throughput in MB/s.
|
||||
|
||||
Notes:
|
||||
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
|
||||
- Keep `NODE_PATH=./out` (handled by the npm script).
|
||||
@@ -62,6 +62,9 @@ jobs:
|
||||
./addons/addon-web-links/lib/* \
|
||||
./addons/addon-web-links/out/* \
|
||||
./addons/addon-web-links/out-*/* \
|
||||
./addons/addon-web-fonts/lib/* \
|
||||
./addons/addon-web-fonts/out/* \
|
||||
./addons/addon-web-fonts/out-*/* \
|
||||
./addons/addon-webgl/lib/* \
|
||||
./addons/addon-webgl/out/* \
|
||||
./addons/addon-webgl/out-*st/*
|
||||
@@ -199,8 +202,10 @@ jobs:
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Build demo
|
||||
run: npm run esbuild-demo
|
||||
- name: Build demo client
|
||||
run: npm run esbuild-demo-client
|
||||
- name: Build demo server
|
||||
run: npm run esbuild-demo-server
|
||||
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
|
||||
- name: Integration tests (addon-attach)
|
||||
@@ -221,6 +226,8 @@ jobs:
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
|
||||
- name: Integration tests (addon-unicode11)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
|
||||
- name: Integration tests (addon-web-fonts)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts
|
||||
- name: Integration tests (addon-web-links)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
|
||||
- name: Integration tests (addon-webgl)
|
||||
|
||||
@@ -3,6 +3,7 @@ node_modules/
|
||||
.lock-wscript
|
||||
lib/
|
||||
out/
|
||||
out-demo/
|
||||
out-test/
|
||||
out-esbuild/
|
||||
out-esbuild-test/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Antonis Kalipetis <akalipetis@sourcelair.com>
|
||||
Antonis Kalipetis <akalipetis@sourcelair.com> <akalipetis@gmail.com>
|
||||
Daniel Imms <daimms@microsoft.com> <tyriar@tyriar.com>
|
||||
Paris Kasidiaris <paris@sourcelair.com>
|
||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||
Thanasis Daglis <thanasis@sourcelair.com>
|
||||
Thanasis Daglis <thanasis@sourcelair.com> <ath.daglis@gmail.com>
|
||||
@@ -1 +0,0 @@
|
||||
NODE_PATH=./out
|
||||
@@ -1,5 +0,0 @@
|
||||
require:
|
||||
- source-map-support/register
|
||||
reporter: spec
|
||||
color: true
|
||||
check-leaks: true
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -61,7 +61,7 @@
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["start"],
|
||||
"stopOnEntry": true,
|
||||
"runtimeVersion": "18",
|
||||
"runtimeVersion": "22",
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "App listening to (http://.*?:[0-9]+)"
|
||||
|
||||
Vendored
+1
-20
@@ -1,23 +1,4 @@
|
||||
{
|
||||
"files.associations": {
|
||||
".eslintrc.json.typings": "jsonc"
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.nyc_output": true
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"mochaExplorer.envPath": ".mocha.env",
|
||||
"mochaExplorer.files": [
|
||||
"out/**/*.test.js",
|
||||
"addons/**/out/*.test.js",
|
||||
"out-*/**/*.test.js",
|
||||
"addons/**/out-*/*.test.js"
|
||||
],
|
||||
"mochaExplorer.watch": [
|
||||
"out/**/*.js",
|
||||
"addons/**/out/*.js",
|
||||
"out-*/**/*.js",
|
||||
"addons/**/out-*/*.js"
|
||||
]
|
||||
"typescript.preferences.quoteStyle": "single"
|
||||
}
|
||||
|
||||
Vendored
+25
-4
@@ -10,13 +10,22 @@
|
||||
"tasks": [
|
||||
// Compound tasks
|
||||
{
|
||||
"label": "Development",
|
||||
"dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo"],
|
||||
"label": "dev",
|
||||
"detail": "Runs all tasks required to run the demo in a single terminal using concurrently. This does not support problem matching.",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "dev (separate terminals)",
|
||||
"detail": "Runs all tasks required to run the demo in separate terminals. This does support problem matching.",
|
||||
"dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo-client", "esbuild-demo-server"],
|
||||
"group": "build"
|
||||
},
|
||||
|
||||
// Demo
|
||||
{
|
||||
@@ -55,9 +64,21 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "esbuild-demo",
|
||||
"label": "esbuild-demo-client",
|
||||
"type": "npm",
|
||||
"script": "esbuild-demo-watch",
|
||||
"script": "esbuild-demo-client-watch",
|
||||
"dependsOn": ["esbuild", "tsc"],
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"presentation": {
|
||||
"group": "xterm-demo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "esbuild-demo-server",
|
||||
"type": "npm",
|
||||
"script": "esbuild-demo-server-watch",
|
||||
"dependsOn": ["esbuild", "tsc"],
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
|
||||
+35
-89
@@ -1,104 +1,50 @@
|
||||
# How to contribute to xterm.js
|
||||
|
||||
- [Opening issues for bug reports or feature requests](#opening-issues)
|
||||
- [Contributing code](#contributing-code)
|
||||
- [Opening issues](#opening-issues)
|
||||
- [Answering discussion questions](#answering-discussion-questions)
|
||||
|
||||
## Contributing code
|
||||
|
||||
You can find issues to work on by looking at issues labeled with [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). It's a good idea to comment on the issue saying that you're looking into it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
|
||||
|
||||
- Fork [xterm.js](https://github.com/xtermjs/xterm.js/) ([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
|
||||
- Make the fix and verify it works in the demo. Be sure to follow thew general code style of the rest of the project.
|
||||
- If your changes are easy to test or likely to regress in the future, add tests. These could be unit or integration (playwright) tests.
|
||||
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
||||
|
||||
> ![TIP]
|
||||
> Don't put more than one feature or fix in a single pull request. The smaller pull requests are the easier they are to review and merge.
|
||||
|
||||
By contributing code to xterm.js you:
|
||||
|
||||
- Agree to license the contributed code under xterm.js' [MIT license](LICENSE).
|
||||
- Confirm that you have the right to contribute and license the code in question. This means that either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.
|
||||
|
||||
## Opening issues
|
||||
|
||||
The preferred way to report bugs or request features is to use
|
||||
[GitHub issues](http://github.com/sourcelair/xterm.js/issues). Before
|
||||
opening an issue, read these pointers.
|
||||
[GitHub issues](http://github.com/xtermjs/xterm.js/issues).
|
||||
|
||||
### Opening issues effectively
|
||||
### Creating great issues
|
||||
|
||||
- Include information about **the browser in which the problem occurred**. Even
|
||||
if you tested several browsers, and the problem occurred in all of them,
|
||||
mention this fact in the bug report. Also include browser version numbers and
|
||||
the operating system that you're on.
|
||||
- Include information about **the browser in which the problem occurred** or the terminal being used. If you tested several browsers and the problem occurred in all of them, mention this fact in the bug report. Also include browser version numbers and the operating system that you're on.
|
||||
- Include the version of xterm.js being used, preferably either with the latest `beta` tagged release on npm or reproducing in the demo on the `master` branch.
|
||||
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the exact steps a maintainer has to take to make the problem occur.
|
||||
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), provide an HTML document that demonstrates the problem.
|
||||
- Be polite and follow [the code of conduct](https://github.com/xtermjs/xterm.js?tab=coc-ov-file#readme).
|
||||
|
||||
- Mention which release of xterm.js you're using. Preferably, try also with
|
||||
the current HEAD of the master branch, to ensure the problem has not already been
|
||||
fixed.
|
||||
### Issue triaging philosophy
|
||||
|
||||
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
|
||||
exact steps a maintainer has to take to make the problem occur.
|
||||
It's pretty common for maintainers of large open source projects to suffer from burnout, especially when needing to triage a large number of incoming issues instead of actually building things. Here are some of the steps we take to try mitigate this:
|
||||
|
||||
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
|
||||
|
||||
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
||||
bottom of the pile.
|
||||
- Support questions live in [GH discussions](https://github.com/xtermjs/xterm.js/discussions), issues may be transfered there without further comment and core maintainers may or may not participate in discussions.
|
||||
- Issues are strictly for well defined features or bugs that are _actionable_.
|
||||
- Sometimes features are out of scope. A common example of this is a niche feature that the pricipal implementation ([VS Code](https://code.visualstudio.com/)) won't leverage and therefore would be difficult to maintain and likely suffer from bitrot. The reporter may not agree with this, but you could always create an addon if that works or maintain your own fork if it comes to that.
|
||||
- If a feature does not have a clear way forward or needs more discussion it may be closed ro moved to a discussion.
|
||||
- If a bug is not easily reproducible it may be closed or moved to a discussion. Generally issues that are labeled are something we want to do or has actionable steps to look into further.
|
||||
|
||||
## Answering discussion questions
|
||||
|
||||
Issues are only meant to track (likely) feature requests and bugs. We use [GitHub Discussions](https://github.com/xtermjs/xterm.js/discussions) for general Q&A as well as discussing possible features. If you want to help out, many questions get asked over at the [discussions page](https://github.com/xtermjs/xterm.js/discussions) which could use an expert as the core team is often stretched thin.
|
||||
|
||||
## Contributing code
|
||||
|
||||
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
|
||||
|
||||
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
|
||||
- Make your changes.
|
||||
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
|
||||
- Follow the general code style of the rest of the project (see below).
|
||||
- Submit a pull request
|
||||
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
||||
Don't put more than one feature/fix in a single pull request.
|
||||
|
||||
By contributing code to xterm.js you:
|
||||
|
||||
- Agree to license the contributed code under xterm.js' [MIT
|
||||
license](LICENSE).
|
||||
|
||||
- Confirm that you have the right to contribute and license the code
|
||||
in question. (Either you hold all rights on the code, or the rights
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Test coverage
|
||||
|
||||
One area that always needs attention is improving out unit test coverage, you can view the code coverage report on [Azure Pipelines](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) by clicking the Code Coverage tab.
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit tests
|
||||
|
||||
Unit tests are run with `npm run test-unit`:
|
||||
|
||||
```sh
|
||||
# All unit tests
|
||||
npm run test-unit
|
||||
|
||||
# Absolute file path
|
||||
npm run test-unit out-esbuild/browser/Terminal.test.js
|
||||
|
||||
# Filter by wildcard
|
||||
npm run test-unit out-esbuild/**/Terminal.test.js
|
||||
|
||||
# Specific addon unit tests tests
|
||||
npm run test-unit addons/addon-image/out-esbuild/*.test.js
|
||||
|
||||
# Multiple files
|
||||
npm run test-unit out-esbuild/**/Terminal.test.js out-esbuild/**/InputHandler.test.js
|
||||
```
|
||||
|
||||
These use mocha to run all `.test.js` files within the esbuild output (`out-esbuild/`).
|
||||
|
||||
### Integration tests
|
||||
|
||||
Integration tests are run with `npm run test-integration`:
|
||||
|
||||
```sh
|
||||
# All integration tests
|
||||
npm run test-integration
|
||||
|
||||
# Core integration tests
|
||||
npm run test-integration --suite=core
|
||||
|
||||
# Specific addon integration tests
|
||||
npm run test-integration --suite=addon-search
|
||||
```
|
||||
|
||||
These use `@playwright/test` to run all tests within the esbuild test output (`out-esbuild-test/`).
|
||||
Issues are only meant to track (likely) feature requests and bugs. We use [GitHub Discussions](https://github.com/xtermjs/xterm.js/discussions) for general Q&A as well as discussing possible features. If you want to help out, many questions get asked over at the [discussions page](https://github.com/xtermjs/xterm.js/discussions) which could use an expert as the core team is often stretched thin.
|
||||
@@ -1,27 +1,27 @@
|
||||
# [](https://xtermjs.org)
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
|
||||
Xterm.js is a frontend component that enables applications to bring fully-featured terminals to their users in the browser. It's used by popular projects such as [VS Code](https://code.visualstudio.com/) (and its forks), [Tabby](https://tabby.sh/) and [Hyper](https://hyper.is/).
|
||||
|
||||
## Features
|
||||
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events.
|
||||
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Performant**: Xterm.js is *really* fast and includes an optional GPU-accelerated renderer.
|
||||
- **Rich Unicode support**: Supports CJK, emojis, and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on.
|
||||
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||
- **Self-contained**: The core library has zero dependencies.
|
||||
- **Accessible**: Screen reader mode and minimum contrast ratio support can be turned on.
|
||||
- **And much more**: Links, theming, custom glyphs, addons, well documented API, etc.
|
||||
|
||||
## What xterm.js is not
|
||||
|
||||
- Xterm.js is not a terminal application that you can download and use on your computer.
|
||||
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
|
||||
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) through a library like [node-pty](https://github.com/microsoft/node-pty).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
|
||||
First, you need to install the module. We ship exclusively through [npm](https://www.npmjs.com), so you need that installed and then add [@xterm/xterm](https://www.npmjs.com/package/@xterm/xterm) as a dependency by running:
|
||||
|
||||
```bash
|
||||
npm install @xterm/xterm
|
||||
npm install --save @xterm/xterm
|
||||
```
|
||||
|
||||
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
@@ -54,12 +54,10 @@ import { Terminal } from '@xterm/xterm';
|
||||
|
||||
### Addons
|
||||
|
||||
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
|
||||
|
||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
|
||||
|
||||
```bash
|
||||
npm i -S @xterm/addon-web-links
|
||||
npm install --save @xterm/addon-web-links
|
||||
```
|
||||
|
||||
Then import the addon, instantiate it and call `Terminal.loadAddon`:
|
||||
@@ -80,50 +78,51 @@ The xterm.js team maintains the following addons, but anyone can build them:
|
||||
- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
|
||||
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
|
||||
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
|
||||
- [`@xterm/addon-ligatures`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-ligatures): Enables rendering of ligatures
|
||||
- [`@xterm/addon-progress`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-progress): Adds support for the progress API (`OSC 9;4`)
|
||||
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
|
||||
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to a VT sequences or HTML
|
||||
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to VT sequences or HTML
|
||||
- [`@xterm/addon-unicode-graphemes`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes): Enhanced unicode support including grapheme clustering (experimental)
|
||||
- [`@xterm/addon-unicode11`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11): Updates character widths to their unicode11 values
|
||||
- [`@xterm/addon-web-fonts`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-fonts): Easily integrate web fonts
|
||||
- [`@xterm/addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-links): Adds web link detection and interaction
|
||||
- [`@xterm/addon-webgl`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl): Renders xterm.js using a `canvas` element's webgl2 context
|
||||
|
||||
## Browser Support
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
|
||||
|
||||
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
|
||||
Since xterm.js is typically implemented as a developer tool, generally only modern evergreen browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*. Xterm.js also works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
|
||||
|
||||
### Node.js Support
|
||||
|
||||
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the serialize addon so it can get all state restored upon reconnection.
|
||||
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs headless in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the [serialize addon](https://www.npmjs.com/package/@xterm/addon-serialize) so it can get all state restored upon reconnection.
|
||||
|
||||
## API
|
||||
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
|
||||
Some APIs may be marked with *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
Stable releases are done on an as needed basis. All current and past releases are available on this repo's [releases page](https://github.com/sourcelair/xterm.js/releases), you can see what's planned for upcoming releases looking through the repository [milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
|
||||
Beta releases are continuously published off the `master` branch. Install the latest beta build with:
|
||||
|
||||
```bash
|
||||
npm install -S @xterm/xterm@beta
|
||||
npm install --save @xterm/xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
|
||||
The principal implementation (VS Code) typically uses the latest or near the latest beta build. Generally they are quite stable but can potentially contain bugs or breaking changes. If stability is very important we recommend using the beta build primarily to test out new features and to verify bug fixes, unless you're tracking what's landing and are comfortable taking that risk.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
|
||||
Read [CONTRIBUTING.md](https://github.com/xtermjs/xterm.js/blob/master/CONTRIBUTING.md) to learn how to contribute to the project.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
Xterm.js is used in many world-class applications to provide great terminal experiences.
|
||||
|
||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
|
||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
||||
@@ -232,12 +231,12 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**EmuDevz**](https://afska.github.io/emudevz): A free coding game where players learn how to build an emulator from scratch.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
|
||||
|
||||
## License Agreement
|
||||
|
||||
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2017-2026, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
@@ -12,7 +12,7 @@ interface IAttachOptions {
|
||||
bidirectional?: boolean;
|
||||
}
|
||||
|
||||
export class AttachAddon implements ITerminalAddon , IAttachApi {
|
||||
export class AttachAddon implements ITerminalAddon, IAttachApi {
|
||||
private _socket: WebSocket;
|
||||
private _bidirectional: boolean;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@xterm/addon-clipboard",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@xterm/addon-clipboard",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-base64": "^3.7.5"
|
||||
}
|
||||
},
|
||||
"node_modules/js-base64": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
|
||||
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
js-base64@^3.7.5:
|
||||
version "3.7.7"
|
||||
resolved "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz"
|
||||
integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
|
||||
@@ -3,9 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
import type { Terminal, ITerminalAddon, IRenderDimensions } from '@xterm/xterm';
|
||||
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { ViewportConstants } from 'browser/shared/Constants';
|
||||
|
||||
interface ITerminalDimensions {
|
||||
@@ -23,7 +22,18 @@ interface ITerminalDimensions {
|
||||
const MINIMUM_COLS = 2;
|
||||
const MINIMUM_ROWS = 1;
|
||||
|
||||
export class FitAddon implements ITerminalAddon , IFitApi {
|
||||
function getWindow(e: Node): Window {
|
||||
if (e?.ownerDocument?.defaultView) {
|
||||
return e.ownerDocument.defaultView;
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
|
||||
return getWindow(el).getComputedStyle(el, null);
|
||||
}
|
||||
|
||||
export class FitAddon implements ITerminalAddon, IFitApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -38,12 +48,8 @@ export class FitAddon implements ITerminalAddon , IFitApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (this._terminal as any)._core;
|
||||
|
||||
// Force a full render
|
||||
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
|
||||
core._renderService.clear();
|
||||
this._terminal.resize(dims.cols, dims.rows);
|
||||
}
|
||||
}
|
||||
@@ -57,11 +63,9 @@ export class FitAddon implements ITerminalAddon , IFitApi {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (this._terminal as any)._core;
|
||||
const dims: IRenderDimensions = core._renderService.dimensions;
|
||||
const dims: IRenderDimensions | undefined = this._terminal.dimensions;
|
||||
|
||||
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
|
||||
if (!dims || dims.css.cell.width === 0 || dims.css.cell.height === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -69,10 +73,10 @@ export class FitAddon implements ITerminalAddon , IFitApi {
|
||||
? 0
|
||||
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(this._terminal.element);
|
||||
const elementStyle = _getComputedStyle(this._terminal.element);
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -30,7 +30,9 @@ const customSettings: IImageAddonOptions = {
|
||||
storageLimit: 128, // FIFO storage limit in MB
|
||||
showPlaceholder: true, // whether to show a placeholder for evicted images
|
||||
iipSupport: true, // enable iTerm IIP support
|
||||
iipSizeLimit: 20000000 // size limit of a single IIP sequence
|
||||
iipSizeLimit: 20000000, // size limit of a single IIP sequence
|
||||
kittySupport: true, // enable Kitty graphics support
|
||||
kittySizeLimit: 20000000 // size limit of a single Kitty sequence
|
||||
}
|
||||
|
||||
// initialization
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 B |
Binary file not shown.
|
After Width: | Height: | Size: 449 B |
Binary file not shown.
|
After Width: | Height: | Size: 131 B |
Generated
-817
@@ -1,817 +0,0 @@
|
||||
{
|
||||
"name": "@xterm/addon-image",
|
||||
"version": "0.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@xterm/addon-image",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
|
||||
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.2",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"colorette": "^2.0.20",
|
||||
"glob": "^10.0.0",
|
||||
"wabt": "^1.0.32"
|
||||
},
|
||||
"bin": {
|
||||
"inwasm": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sixel": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/sixel/-/sixel-0.16.0.tgz",
|
||||
"integrity": "sha512-xicu6Y6Cyhmv5rjyHxq2r5RnKerlL/nyZEGjOU5bLCshXkZryc9JFJThTCKPOAtWXCfeWquEKFVFfMPcTD25PA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wabt": {
|
||||
"version": "1.0.39",
|
||||
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
|
||||
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"wasm-decompile": "bin/wasm-decompile",
|
||||
"wasm-interp": "bin/wasm-interp",
|
||||
"wasm-objdump": "bin/wasm-objdump",
|
||||
"wasm-stats": "bin/wasm-stats",
|
||||
"wasm-strip": "bin/wasm-strip",
|
||||
"wasm-validate": "bin/wasm-validate",
|
||||
"wasm2c": "bin/wasm2c",
|
||||
"wasm2wat": "bin/wasm2wat",
|
||||
"wat2wasm": "bin/wat2wasm"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/xterm-wasm-parts": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
|
||||
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inwasm": "^0.0.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare const Buffer: any;
|
||||
|
||||
// limit hold memory in base64 decoder
|
||||
// limit hold memory in base64 decoder (encoded bytes)
|
||||
const KEEP_DATA = 4194304;
|
||||
const INITIAL_DATA = 1048576;
|
||||
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
const DECODER_OK: DecodeStatus.OK = 0;
|
||||
|
||||
// default IIP header values
|
||||
const DEFAULT_HEADER: IHeaderFields = {
|
||||
@@ -31,7 +31,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _aborted = false;
|
||||
private _hp = new HeaderParser();
|
||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||
private _dec = new Base64Decoder(KEEP_DATA);
|
||||
private _dec: Base64Decoder;
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
|
||||
constructor(
|
||||
@@ -39,7 +39,11 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {}
|
||||
) {
|
||||
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
||||
const initialBytes = Math.min(INITIAL_DATA, maxEncodedBytes);
|
||||
this._dec = new Base64Decoder(KEEP_DATA, maxEncodedBytes, initialBytes);
|
||||
}
|
||||
|
||||
public reset(): void {}
|
||||
|
||||
@@ -54,7 +58,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (this._hp.state === HeaderState.END) {
|
||||
if (this._dec.put(data, start, end)) {
|
||||
if (this._dec.put(data.subarray(start, end)) !== DECODER_OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -70,8 +74,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._dec.init(this._header.size);
|
||||
if (this._dec.put(data, dataPos, end)) {
|
||||
this._dec.init();
|
||||
if (this._dec.put(data.subarray(dataPos, end)) !== DECODER_OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -89,13 +93,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
let cond: number | boolean = true;
|
||||
if (cond = success) {
|
||||
if (cond = !this._dec.end()) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
if (cond = this._dec.data8.length === this._header.size) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +111,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
||||
// HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
|
||||
const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
|
||||
this._dec.release();
|
||||
|
||||
if (!window.createImageBitmap) {
|
||||
|
||||
@@ -8,6 +8,7 @@ declare const Buffer: any;
|
||||
|
||||
|
||||
export interface IHeaderFields {
|
||||
[key: string]: number | string | Uint32Array | null | undefined;
|
||||
// base-64 encoded filename. Defaults to "Unnamed file".
|
||||
name: string;
|
||||
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
||||
@@ -81,7 +82,7 @@ function toName(data: Uint32Array): string {
|
||||
return new TextDecoder().decode(b);
|
||||
}
|
||||
|
||||
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
|
||||
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
||||
inline: toInt,
|
||||
size: toInt,
|
||||
name: toName,
|
||||
@@ -100,7 +101,7 @@ export class HeaderParser {
|
||||
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
||||
private _position = 0;
|
||||
private _key = '';
|
||||
public fields: {[key: string]: any} = {};
|
||||
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
|
||||
|
||||
public reset(): void {
|
||||
this._buffer.fill(0);
|
||||
@@ -176,7 +177,7 @@ export class HeaderParser {
|
||||
try {
|
||||
const v = this._buffer.slice(0, pos);
|
||||
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ImageAddon as IImageApi } from '@xterm/addon-image';
|
||||
import { IIPHandler } from './IIPHandler';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
|
||||
import { SixelHandler } from './SixelHandler';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
@@ -22,7 +23,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
iipSizeLimit: 20000000,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
};
|
||||
|
||||
// max palette size supported by the sixel lib (compile time setting)
|
||||
@@ -48,7 +51,7 @@ const enum GaStatus {
|
||||
}
|
||||
|
||||
|
||||
export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
private _opts: IImageAddonOptions;
|
||||
private _defaultOpts: IImageAddonOptions;
|
||||
private _storage: ImageStorage | undefined;
|
||||
@@ -90,7 +93,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
// windowOptions.getCellSizePixels = true;
|
||||
// windowOptions.getWinSizeChars = true;
|
||||
// terminal.setOption('windowOptions', windowOptions);
|
||||
const windowOps = terminal.options.windowOptions || {};
|
||||
const windowOps = terminal.options.windowOptions ?? {};
|
||||
windowOps.getWinSizePixels = true;
|
||||
windowOps.getCellSizePixels = true;
|
||||
windowOps.getWinSizeChars = true;
|
||||
@@ -144,6 +147,15 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// Kitty graphics handler
|
||||
if (this._opts.kittySupport) {
|
||||
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, this._storage!, terminal);
|
||||
this._handlers.set('kitty', kittyHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
||||
|
||||
@@ -38,7 +38,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Only the DOM output canvas should be on the terminal's document,
|
||||
* which gets explicitly checked in `insertLayerToDom`.
|
||||
*/
|
||||
const canvas = (localDocument || document).createElement('canvas');
|
||||
const canvas = (localDocument ?? document).createElement('canvas');
|
||||
canvas.width = width | 0;
|
||||
canvas.height = height | 0;
|
||||
return canvas;
|
||||
@@ -124,7 +124,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Forwarded from internal render service.
|
||||
*/
|
||||
public get dimensions(): IRenderDimensions | undefined {
|
||||
return this._renderService?.dimensions;
|
||||
return this._terminal.dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +242,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
}
|
||||
if (!this._placeholder) return;
|
||||
this._ctx.drawImage(
|
||||
this._placeholderBitmap || this._placeholder!,
|
||||
this._placeholderBitmap ?? this._placeholder!,
|
||||
col * width,
|
||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
||||
width * count,
|
||||
|
||||
@@ -132,8 +132,10 @@ export class ImageStorage implements IDisposable {
|
||||
) {
|
||||
try {
|
||||
this.setLimit(this._opts.storageLimit);
|
||||
} catch (e: any) {
|
||||
console.error(e.message);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
console.error(e.message);
|
||||
}
|
||||
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
||||
}
|
||||
this._viewportMetrics = {
|
||||
@@ -233,9 +235,22 @@ export class ImageStorage implements IDisposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
* Delete an image by its internal storage ID.
|
||||
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
public deleteImage(id: number): void {
|
||||
const spec = this._images.get(id);
|
||||
if (spec) {
|
||||
spec.marker?.dispose();
|
||||
this._delImg(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
* Returns the internal image ID assigned to the stored image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): number {
|
||||
// never allow storage to exceed memory limit
|
||||
this._evictOldest(img.width * img.height);
|
||||
|
||||
@@ -329,6 +344,7 @@ export class ImageStorage implements IDisposable {
|
||||
|
||||
// finally add the image
|
||||
this._images.set(imageId, imgSpec);
|
||||
return imageId;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,7 +397,7 @@ export class ImageStorage implements IDisposable {
|
||||
if (!line) return;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
@@ -400,7 +416,7 @@ export class ImageStorage implements IDisposable {
|
||||
while (
|
||||
++col < cols
|
||||
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
||||
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
|
||||
&& (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
|
||||
&& (e.imageId === imageId)
|
||||
&& (e.tileId === startTile + count)
|
||||
) {
|
||||
@@ -442,7 +458,7 @@ export class ImageStorage implements IDisposable {
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
@@ -487,7 +503,7 @@ export class ImageStorage implements IDisposable {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1) {
|
||||
const orig = this._images.get(e.imageId)?.orig;
|
||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||
@@ -507,7 +523,7 @@ export class ImageStorage implements IDisposable {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||
const spec = this._images.get(e.imageId);
|
||||
if (spec) {
|
||||
|
||||
@@ -99,7 +99,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
||||
}
|
||||
|
||||
const canvas = ImageRenderer.createCanvas(undefined, width, height);
|
||||
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
|
||||
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
|
||||
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
||||
this._dec.release();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
|
||||
// private imports from base repo we build against
|
||||
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import type { AttributeData } from 'common/buffer/AttributeData';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
||||
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
@@ -22,7 +22,7 @@ export const enum Cell {
|
||||
}
|
||||
|
||||
// export some privates for local usage
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
|
||||
/**
|
||||
* Plugin ctor options.
|
||||
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
}
|
||||
|
||||
export interface IResetHandler {
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt } from '../Types';
|
||||
import { ImageRenderer } from '../ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from '../ImageStorage';
|
||||
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import {
|
||||
KittyAction,
|
||||
KittyFormat,
|
||||
KittyCompression,
|
||||
IKittyCommand,
|
||||
IPendingTransmission,
|
||||
IKittyImageData,
|
||||
BYTES_PER_PIXEL_RGB,
|
||||
BYTES_PER_PIXEL_RGBA,
|
||||
ALPHA_OPAQUE,
|
||||
parseKittyCommand
|
||||
} from './KittyGraphicsTypes';
|
||||
|
||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||
const DECODER_KEEP_DATA = 4194304;
|
||||
const DECODER_INITIAL_DATA = 4194304; // 4MB
|
||||
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
const DECODER_OK: DecodeStatus.OK = 0;
|
||||
|
||||
// Maximum control data size
|
||||
const MAX_CONTROL_DATA_SIZE = 512;
|
||||
|
||||
// Semicolon codepoint
|
||||
const SEMICOLON = 0x3B;
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol handler with streaming base64 decoding.
|
||||
*/
|
||||
export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
private _aborted = false;
|
||||
private _decodeError = false;
|
||||
|
||||
private _activeDecoder: Base64Decoder | null = null;
|
||||
private readonly _maxEncodedBytes: number;
|
||||
private readonly _initialEncodedBytes: number;
|
||||
|
||||
// Streaming related states
|
||||
|
||||
/** True while receiving control data (before semicolon). */
|
||||
private _inControlData = true;
|
||||
|
||||
/** Buffer for control data. */
|
||||
private _controlData = new Uint32Array(MAX_CONTROL_DATA_SIZE);
|
||||
private _controlLength = 0;
|
||||
|
||||
/** Pre-calculated encoded size limit */
|
||||
private _encodedSizeLimit = 0;
|
||||
private _totalEncodedSize = 0;
|
||||
|
||||
/** Parsed command. These are the control data before semicolon. */
|
||||
private _parsedCommand: IKittyCommand | null = null;
|
||||
|
||||
// Storage related states
|
||||
|
||||
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
|
||||
private _nextImageId = 1;
|
||||
/** Maps Kitty protocol image ID → ImageStorage internal ID for deletion/lookup. */
|
||||
private _kittyIdToStorageId: Map<number, number> = new Map();
|
||||
// TODO: Eliminate double storage — raw image data lives here (as Blob) AND rendered
|
||||
// ImageBitmaps live in ImageStorage. Currently we only use ImageStorage.addImage(bitmap)
|
||||
// for tiling + cursor movement + marker-based eviction.
|
||||
//
|
||||
|
||||
// See: https://github.com/xtermjs/xterm.js/pull/5619#issuecomment-3853678815
|
||||
private _images: Map<number, IKittyImageData> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {
|
||||
// Convert decoded size limit -> max encoded bytes.
|
||||
this._maxEncodedBytes = Math.ceil(this._opts.kittySizeLimit * 4 / 3);
|
||||
// ensure we preallocate more than configured limit while using 4mb initial size.
|
||||
this._initialEncodedBytes = Math.min(DECODER_INITIAL_DATA, this._maxEncodedBytes);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
this._images.clear();
|
||||
this._kittyIdToStorageId.clear();
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this._aborted = false;
|
||||
this._decodeError = false;
|
||||
this._inControlData = true;
|
||||
this._controlLength = 0;
|
||||
this._parsedCommand = null;
|
||||
// Pre-calculate encoded limit once: base64 is 4 bytes encoded → 3 bytes decoded
|
||||
this._encodedSizeLimit = this._maxEncodedBytes;
|
||||
this._totalEncodedSize = 0;
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (!this._inControlData) {
|
||||
this._streamPayload(data, start, end);
|
||||
} else {
|
||||
// Scan for semicolon
|
||||
let controlEnd = end;
|
||||
for (let i = start; i < end; i++) {
|
||||
if (data[i] === SEMICOLON) {
|
||||
this._inControlData = false;
|
||||
controlEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy control data
|
||||
const copyLength = controlEnd - start;
|
||||
if (this._controlLength + copyLength > MAX_CONTROL_DATA_SIZE) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._controlData.set(data.subarray(start, controlEnd), this._controlLength);
|
||||
this._controlLength += copyLength;
|
||||
|
||||
if (!this._inControlData) {
|
||||
// Found semicolon - parse control data early for validation
|
||||
this._parsedCommand = parseKittyCommand(this._parseControlDataString());
|
||||
|
||||
// Early validation: i+I conflict
|
||||
if (this._parsedCommand.id !== undefined && this._parsedCommand.imageNumber !== undefined) {
|
||||
this._sendResponse(this._parsedCommand.id, 'EINVAL:cannot specify both i and I keys', this._parsedCommand.quiet ?? 0);
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete action doesn't need payload - skip streaming
|
||||
if (this._parsedCommand.action === KittyAction.DELETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream remaining as payload
|
||||
const payloadStart = controlEnd + 1;
|
||||
if (payloadStart < end) {
|
||||
this._streamPayload(data, payloadStart, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream payload bytes into the base64 decoder.
|
||||
*/
|
||||
private _streamPayload(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
// Check size limit (compare encoded bytes against pre-calculated limit)
|
||||
// Include cumulative size from pending transmission for multi-chunk images
|
||||
const pendingKey = this._parsedCommand?.id ?? 0;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
|
||||
this._totalEncodedSize += end - start;
|
||||
const cumulativeEncodedSize = previousEncodedSize + this._totalEncodedSize;
|
||||
if (cumulativeEncodedSize > this._encodedSizeLimit) {
|
||||
const decoderToRelease = this._activeDecoder ?? pending?.decoder;
|
||||
if (decoderToRelease) {
|
||||
decoderToRelease.release();
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
if (pending) {
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
}
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._decodeError) return;
|
||||
|
||||
if (pending?.decoder && !this._activeDecoder) {
|
||||
this._activeDecoder = pending.decoder;
|
||||
}
|
||||
if (!this._activeDecoder) {
|
||||
this._activeDecoder = new Base64Decoder(DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
|
||||
this._activeDecoder.init();
|
||||
}
|
||||
|
||||
if (this._activeDecoder.put(data.subarray(start, end)) !== DECODER_OK) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
this._decodeError = true;
|
||||
if (pending) {
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
if (this._aborted || !success) {
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// No semicolon = no payload (delete, capability query)
|
||||
if (this._inControlData) {
|
||||
return this._handleNoPayloadCommand();
|
||||
}
|
||||
|
||||
// Use command parsed early in put() - i+I already validated there
|
||||
const cmd = this._parsedCommand!;
|
||||
|
||||
// Delete action was handled by skipping payload - just execute
|
||||
if (cmd.action === KittyAction.DELETE) {
|
||||
return this._handleDelete(cmd);
|
||||
}
|
||||
|
||||
const pendingKey = cmd.id ?? 0;
|
||||
const isMoreComing = cmd.more === 1;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
|
||||
if (isMoreComing) {
|
||||
if (this._activeDecoder) {
|
||||
if (pending) {
|
||||
pending.totalEncodedSize += this._totalEncodedSize;
|
||||
pending.decodeError = pending.decodeError || this._decodeError;
|
||||
} else {
|
||||
this._pendingTransmissions.set(pendingKey, {
|
||||
cmd: { ...cmd },
|
||||
decoder: this._activeDecoder,
|
||||
totalEncodedSize: this._totalEncodedSize,
|
||||
decodeError: this._decodeError
|
||||
});
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let decodeError = this._decodeError;
|
||||
let finalCmd = cmd;
|
||||
let decoder = this._activeDecoder;
|
||||
|
||||
if (pending) {
|
||||
finalCmd = pending.cmd;
|
||||
decoder = pending.decoder;
|
||||
decodeError = decodeError || pending.decodeError;
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
}
|
||||
|
||||
let imageBytes = new Uint8Array(0);
|
||||
if (decoder) {
|
||||
if (decoder.end() !== DECODER_OK) {
|
||||
decodeError = true;
|
||||
}
|
||||
imageBytes = decoder.data8;
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
|
||||
// Handle command first — handlers create Blob/ImageData from imageBytes,
|
||||
// which copies the data. Only then is it safe to release the decoder's
|
||||
// wasm memory that imageBytes points into.
|
||||
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
|
||||
if (decoder) {
|
||||
decoder.release();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Command handling
|
||||
|
||||
private _parseControlDataString(): string {
|
||||
let str = '';
|
||||
for (let i = 0; i < this._controlLength; i++) {
|
||||
str += String.fromCodePoint(this._controlData[i]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
private _handleNoPayloadCommand(): boolean | Promise<boolean> {
|
||||
const cmd = parseKittyCommand(this._parseControlDataString());
|
||||
|
||||
// Per spec: specifying both i and I is an error
|
||||
if (cmd.id !== undefined && cmd.imageNumber !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
const action = cmd.action ?? 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.DELETE:
|
||||
return this._handleDelete(cmd);
|
||||
case KittyAction.QUERY:
|
||||
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleCommandWithBytesAndCmd(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
const action = cmd.action ?? 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.TRANSMIT: {
|
||||
const result = this._handleTransmit(cmd, bytes, decodeError);
|
||||
if (cmd.id !== undefined && !decodeError && bytes.length > 0) {
|
||||
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case KittyAction.TRANSMIT_DISPLAY:
|
||||
return this._handleTransmitDisplay(cmd, bytes, decodeError);
|
||||
case KittyAction.QUERY:
|
||||
return this._handleQuery(cmd, bytes, decodeError);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
|
||||
// Currently only supports direct transmission (t=d, the default).
|
||||
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
|
||||
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
|
||||
// - t=s Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
|
||||
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
|
||||
// 1. Check cmd.transmission (t key) before treating bytes as image data
|
||||
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
|
||||
// 3. For t=d: treat bytes as image data (current behavior)
|
||||
|
||||
if (decodeError || bytes.length === 0) return true;
|
||||
|
||||
const id = cmd.id ?? this._nextImageId++;
|
||||
const image: IKittyImageData = {
|
||||
id,
|
||||
data: new Blob([bytes as BlobPart]),
|
||||
width: cmd.width ?? 0,
|
||||
height: cmd.height ?? 0,
|
||||
format: (cmd.format ?? KittyFormat.RGBA) as 24 | 32 | 100,
|
||||
compression: cmd.compression ?? ''
|
||||
};
|
||||
this._images.set(id, image);
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
if (decodeError) return true;
|
||||
|
||||
const pendingKey = cmd.id ?? 0;
|
||||
|
||||
this._handleTransmit(cmd, bytes, decodeError);
|
||||
|
||||
// If still accumulating chunks, don't display yet
|
||||
if (this._pendingTransmissions.has(pendingKey)) return true;
|
||||
|
||||
// Display the completed image
|
||||
const id = cmd.id ?? this._nextImageId - 1;
|
||||
const image = this._images.get(id);
|
||||
if (image) {
|
||||
const result = this._displayImage(image, cmd);
|
||||
if (cmd.id !== undefined) {
|
||||
return (result as Promise<boolean>).then(r => {
|
||||
this._sendResponse(id, 'OK', cmd.quiet ?? 0);
|
||||
return r;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleQuery(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
const id = cmd.id ?? 0;
|
||||
const quiet = cmd.quiet ?? 0;
|
||||
|
||||
// Check decode error first (invalid base64)
|
||||
if (decodeError) {
|
||||
this._sendResponse(id, 'EINVAL:invalid base64 data', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Capability query (no payload) - just respond OK
|
||||
if (bytes.length === 0) {
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const format = cmd.format ?? KittyFormat.RGBA;
|
||||
|
||||
if (format === KittyFormat.PNG) {
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
} else {
|
||||
const width = cmd.width ?? 0;
|
||||
const height = cmd.height ?? 0;
|
||||
|
||||
if (!width || !height) {
|
||||
this._sendResponse(id, 'EINVAL:width and height required for raw pixel data', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const bytesPerPixel = format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
this._sendResponse(id, `EINVAL:insufficient pixel data`, quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleDelete(cmd: IKittyCommand): boolean {
|
||||
const id = cmd.id;
|
||||
|
||||
if (id !== undefined) {
|
||||
// Abort in-flight chunked upload for this specific image
|
||||
const pending = this._pendingTransmissions.get(id);
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
this._pendingTransmissions.delete(id);
|
||||
}
|
||||
|
||||
this._images.delete(id);
|
||||
const storageId = this._kittyIdToStorageId.get(id);
|
||||
if (storageId !== undefined) {
|
||||
this._storage.deleteImage(storageId);
|
||||
this._kittyIdToStorageId.delete(id);
|
||||
}
|
||||
} else {
|
||||
// Abort all in-flight chunked uploads
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
|
||||
this._images.clear();
|
||||
for (const storageId of this._kittyIdToStorageId.values()) {
|
||||
this._storage.deleteImage(storageId);
|
||||
}
|
||||
this._kittyIdToStorageId.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _sendResponse(id: number, message: string, quiet: number): void {
|
||||
const isOk = message === 'OK';
|
||||
if (isOk && quiet === 1) return;
|
||||
if (!isOk && quiet === 2) return;
|
||||
|
||||
const response = `\x1b_Gi=${id};${message}\x1b\\`;
|
||||
this._coreTerminal._core.coreService.triggerDataEvent(response);
|
||||
}
|
||||
|
||||
// Image display
|
||||
|
||||
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): boolean | Promise<boolean> {
|
||||
return this._decodeAndDisplay(image, cmd)
|
||||
.then(() => true)
|
||||
.catch(() => true);
|
||||
}
|
||||
|
||||
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
|
||||
const bitmap = await this._createBitmap(image);
|
||||
|
||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
||||
|
||||
// Per spec: c/r default to image's natural cell dimensions
|
||||
const imgCols = cmd.columns ?? Math.ceil(bitmap.width / cw);
|
||||
const imgRows = cmd.rows ?? Math.ceil(bitmap.height / ch);
|
||||
|
||||
let w = bitmap.width;
|
||||
let h = bitmap.height;
|
||||
|
||||
// Scale bitmap to fit placement rectangle when c/r are specified
|
||||
if (cmd.columns !== undefined || cmd.rows !== undefined) {
|
||||
w = Math.round(imgCols * cw);
|
||||
h = Math.round(imgRows * ch);
|
||||
}
|
||||
|
||||
if (w * h > this._opts.pixelLimit) return;
|
||||
|
||||
// Save cursor position before addImage modifies it
|
||||
const buffer = this._coreTerminal._core.buffer;
|
||||
const savedX = buffer.x;
|
||||
const savedY = buffer.y;
|
||||
const savedYbase = buffer.ybase;
|
||||
|
||||
let storageId: number;
|
||||
if (w !== bitmap.width || h !== bitmap.height) {
|
||||
const resized = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
|
||||
storageId = this._storage.addImage(resized);
|
||||
} else {
|
||||
storageId = this._storage.addImage(bitmap);
|
||||
}
|
||||
this._kittyIdToStorageId.set(image.id, storageId);
|
||||
|
||||
// Kitty cursor movement
|
||||
// Per spec: cursor placed at first column after last image column,
|
||||
// on the last row of the image. C=1 means don't move cursor.
|
||||
if (cmd.cursorMovement === 1) {
|
||||
// C=1: restore cursor to position before image was placed
|
||||
const scrolled = buffer.ybase - savedYbase;
|
||||
buffer.x = savedX;
|
||||
// Can't restore cursor to scrollback?
|
||||
buffer.y = Math.max(savedY - scrolled, 0);
|
||||
} else {
|
||||
// Default (C=0): advance cursor horizontally past the image
|
||||
// addImage already positioned cursor on the last row via lineFeeds
|
||||
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ImageBitmap from already-decoded image data.
|
||||
*/
|
||||
private async _createBitmap(image: IKittyImageData): Promise<ImageBitmap> {
|
||||
let bytes: Uint8Array = new Uint8Array(await image.data.arrayBuffer());
|
||||
|
||||
if (image.compression === KittyCompression.ZLIB) {
|
||||
bytes = await this._decompressZlib(bytes);
|
||||
}
|
||||
|
||||
if (image.format === KittyFormat.PNG) {
|
||||
const blob = new Blob([bytes as BlobPart], { type: 'image/png' });
|
||||
if (!window.createImageBitmap) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
return new Promise<ImageBitmap>((resolve, reject) => {
|
||||
img.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
|
||||
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
||||
createImageBitmap(canvas).then(resolve).catch(reject);
|
||||
});
|
||||
img.addEventListener('error', reject);
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
return createImageBitmap(blob);
|
||||
}
|
||||
|
||||
// Raw pixel data
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
|
||||
if (!width || !height) {
|
||||
throw new Error('Width and height required for raw pixel data');
|
||||
}
|
||||
|
||||
const bytesPerPixel = image.format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
throw new Error('Insufficient pixel data');
|
||||
}
|
||||
|
||||
const pixelCount = width * height;
|
||||
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
|
||||
const isRgba = image.format === KittyFormat.RGBA;
|
||||
|
||||
let srcOffset = 0;
|
||||
let dstOffset = 0;
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
data[dstOffset] = bytes[srcOffset];
|
||||
data[dstOffset + 1] = bytes[srcOffset + 1];
|
||||
data[dstOffset + 2] = bytes[srcOffset + 2];
|
||||
data[dstOffset + 3] = isRgba ? bytes[srcOffset + 3] : ALPHA_OPAQUE;
|
||||
srcOffset += bytesPerPixel;
|
||||
dstOffset += BYTES_PER_PIXEL_RGBA;
|
||||
}
|
||||
|
||||
return createImageBitmap(new ImageData(data, width, height));
|
||||
}
|
||||
|
||||
private async _decompressZlib(compressed: Uint8Array): Promise<Uint8Array> {
|
||||
try {
|
||||
return await this._decompress(compressed, 'deflate');
|
||||
} catch {
|
||||
return await this._decompress(compressed, 'deflate-raw');
|
||||
}
|
||||
}
|
||||
|
||||
private async _decompress(compressed: Uint8Array, format: 'deflate' | 'deflate-raw'): Promise<Uint8Array> {
|
||||
const ds = new DecompressionStream(format);
|
||||
const writer = ds.writable.getWriter();
|
||||
writer.write(compressed as BufferSource);
|
||||
writer.close();
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
const reader = ds.readable.getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public get images(): ReadonlyMap<number, IKittyImageData> {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
|
||||
return this._pendingTransmissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { parseKittyCommand, KittyAction, KittyFormat } from './KittyGraphicsTypes';
|
||||
|
||||
describe('KittyGraphicsTypes', () => {
|
||||
describe('parseKittyCommand', () => {
|
||||
it('should parse control data with action and format', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.strictEqual(cmd.action, 'T');
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should parse control data with all options', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,i=5,s=10,v=20,c=3,r=2,m=1,q=2');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.strictEqual(cmd.format, 32);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
assert.strictEqual(cmd.width, 10);
|
||||
assert.strictEqual(cmd.height, 20);
|
||||
assert.strictEqual(cmd.columns, 3);
|
||||
assert.strictEqual(cmd.rows, 2);
|
||||
assert.strictEqual(cmd.more, 1);
|
||||
assert.strictEqual(cmd.quiet, 2);
|
||||
});
|
||||
|
||||
it('should handle empty control data', () => {
|
||||
const cmd = parseKittyCommand('');
|
||||
assert.strictEqual(cmd.action, undefined);
|
||||
assert.strictEqual(cmd.format, undefined);
|
||||
});
|
||||
|
||||
it('should parse transmit action', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100');
|
||||
assert.strictEqual(cmd.action, KittyAction.TRANSMIT);
|
||||
assert.strictEqual(cmd.format, KittyFormat.PNG);
|
||||
});
|
||||
|
||||
it('should parse delete action', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.strictEqual(cmd.action, KittyAction.DELETE);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse empty action as empty string', () => {
|
||||
const cmd = parseKittyCommand('a=,f=100');
|
||||
assert.strictEqual(cmd.action, '');
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should leave action undefined when key is not present', () => {
|
||||
const cmd = parseKittyCommand('f=100,i=5');
|
||||
assert.strictEqual(cmd.action, undefined);
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse compression key', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,o=z');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.strictEqual(cmd.format, 32);
|
||||
assert.strictEqual(cmd.compression, 'z');
|
||||
});
|
||||
|
||||
it('should parse cursor movement key', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,C=1');
|
||||
assert.strictEqual(cmd.cursorMovement, 1);
|
||||
});
|
||||
|
||||
it('should parse cursor movement key C=0', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,C=0');
|
||||
assert.strictEqual(cmd.cursorMovement, 0);
|
||||
});
|
||||
|
||||
it('should parse x and y offset', () => {
|
||||
const cmd = parseKittyCommand('a=T,x=10,y=20');
|
||||
assert.strictEqual(cmd.x, 10);
|
||||
assert.strictEqual(cmd.y, 20);
|
||||
});
|
||||
|
||||
it('should handle keys without values', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=,i=5');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.ok(isNaN(cmd.format!));
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Kitty graphics protocol types, constants, and parsing utilities.
|
||||
*/
|
||||
|
||||
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol action types.
|
||||
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
|
||||
*/
|
||||
export const enum KittyAction {
|
||||
TRANSMIT = 't',
|
||||
TRANSMIT_DISPLAY = 'T',
|
||||
QUERY = 'q',
|
||||
PLACEMENT = 'p',
|
||||
DELETE = 'd'
|
||||
}
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol format types.
|
||||
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
|
||||
*/
|
||||
export const enum KittyFormat {
|
||||
RGB = 24,
|
||||
RGBA = 32,
|
||||
PNG = 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol compression types.
|
||||
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
|
||||
*/
|
||||
export const enum KittyCompression {
|
||||
NONE = '',
|
||||
ZLIB = 'z'
|
||||
}
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol control data keys.
|
||||
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
|
||||
*/
|
||||
export const enum KittyKey {
|
||||
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
|
||||
ACTION = 'a',
|
||||
// Image format (24=RGB, 32=RGBA, 100=PNG)
|
||||
FORMAT = 'f',
|
||||
// Image ID for referencing stored images
|
||||
ID = 'i',
|
||||
// Image number (alternative to ID, terminal assigns ID)
|
||||
IMAGE_NUMBER = 'I',
|
||||
// Source image width in pixels
|
||||
WIDTH = 's',
|
||||
// Source image height in pixels
|
||||
HEIGHT = 'v',
|
||||
// The left edge (in pixels) of the image area to display
|
||||
X_OFFSET = 'x',
|
||||
// The top edge (in pixels) of the image area to display
|
||||
Y_OFFSET = 'y',
|
||||
// Number of terminal columns to display the image over
|
||||
COLUMNS = 'c',
|
||||
// Number of terminal rows to display the image over
|
||||
ROWS = 'r',
|
||||
// More data flag (1=more chunks coming, 0=final chunk)
|
||||
MORE = 'm',
|
||||
// Compression type (z=zlib). This is essential for chunking larger images.
|
||||
COMPRESSION = 'o',
|
||||
// Quiet mode (1=suppress OK responses, 2=suppress error responses)
|
||||
QUIET = 'q',
|
||||
// Cursor movement policy (0=move cursor after image, 1=don't move cursor)
|
||||
CURSOR_MOVEMENT = 'C'
|
||||
}
|
||||
|
||||
// Pixel format constants
|
||||
export const BYTES_PER_PIXEL_RGB = 3;
|
||||
export const BYTES_PER_PIXEL_RGBA = 4;
|
||||
export const ALPHA_OPAQUE = 255;
|
||||
|
||||
/**
|
||||
* Parsed Kitty graphics command.
|
||||
*/
|
||||
export interface IKittyCommand {
|
||||
action?: string;
|
||||
format?: number;
|
||||
id?: number;
|
||||
imageNumber?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
more?: number;
|
||||
quiet?: number;
|
||||
cursorMovement?: number;
|
||||
compression?: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending chunked transmission state.
|
||||
* Stores metadata from the first chunk while accumulating decoded payload data.
|
||||
*/
|
||||
export interface IPendingTransmission {
|
||||
/** The parsed command from the first chunk (contains action, format, dimensions, etc.) */
|
||||
cmd: IKittyCommand;
|
||||
/** Decoder used across chunked payloads */
|
||||
decoder: Base64Decoder;
|
||||
/** Total encoded (base64) bytes received across all chunks - for size limit enforcement */
|
||||
totalEncodedSize: number;
|
||||
/** Whether any chunk has failed to decode */
|
||||
decodeError: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored Kitty image data.
|
||||
*/
|
||||
export interface IKittyImageData {
|
||||
id: number;
|
||||
/** Decoded image data stored as Blob (off JS heap) to avoid 2GB heap limit */
|
||||
data: Blob;
|
||||
width: number;
|
||||
height: number;
|
||||
format: 24 | 32 | 100;
|
||||
compression?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Kitty graphics control data into a command object.
|
||||
*/
|
||||
export function parseKittyCommand(data: string): IKittyCommand {
|
||||
const cmd: IKittyCommand = {};
|
||||
const parts = data.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const eqIdx = part.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
|
||||
const key = part.substring(0, eqIdx);
|
||||
const value = part.substring(eqIdx + 1);
|
||||
|
||||
// Handle string keys first
|
||||
if (key === KittyKey.ACTION) {
|
||||
cmd.action = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.COMPRESSION) {
|
||||
cmd.compression = value;
|
||||
continue;
|
||||
}
|
||||
const numValue = parseInt(value);
|
||||
switch (key) {
|
||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||
case KittyKey.ID: cmd.id = numValue; break;
|
||||
case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break;
|
||||
case KittyKey.WIDTH: cmd.width = numValue; break;
|
||||
case KittyKey.HEIGHT: cmd.height = numValue; break;
|
||||
case KittyKey.X_OFFSET: cmd.x = numValue; break;
|
||||
case KittyKey.Y_OFFSET: cmd.y = numValue; break;
|
||||
case KittyKey.COLUMNS: cmd.columns = numValue; break;
|
||||
case KittyKey.ROWS: cmd.rows = numValue; break;
|
||||
case KittyKey.MORE: cmd.more = numValue; break;
|
||||
case KittyKey.QUIET: cmd.quiet = numValue; break;
|
||||
case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
|
||||
}
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -23,6 +23,8 @@ export interface IImageAddonOptions {
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -134,7 +136,9 @@ test.describe('ImageAddon', () => {
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
iipSizeLimit: 20000000,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
};
|
||||
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
|
||||
});
|
||||
@@ -149,7 +153,9 @@ test.describe('ImageAddon', () => {
|
||||
storageLimit: 10,
|
||||
showPlaceholder: false,
|
||||
iipSupport: false,
|
||||
iipSizeLimit: 1000
|
||||
iipSizeLimit: 1000,
|
||||
kittySupport: false,
|
||||
kittySizeLimit: 1000
|
||||
};
|
||||
await ctx.page.evaluate(opts => {
|
||||
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
|
||||
@@ -300,7 +306,7 @@ test.describe('ImageAddon', () => {
|
||||
* terminal access helpers.
|
||||
*/
|
||||
async function getDimensions(): Promise<IDimensions> {
|
||||
const dimensions: any = await ctx.page.evaluate(`term._core._renderService.dimensions`);
|
||||
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
||||
return {
|
||||
cellWidth: Math.round(dimensions.css.cell.width),
|
||||
cellHeight: Math.round(dimensions.css.cell.height),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+9
@@ -76,6 +76,15 @@ declare module '@xterm/addon-image' {
|
||||
iipSupport?: boolean;
|
||||
/** IIP sequence size limit (default 20000000 bytes). */
|
||||
iipSizeLimit?: number;
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol settings
|
||||
*/
|
||||
|
||||
/** Whether Kitty graphics protocol is enabled (default is true). */
|
||||
kittySupport?: boolean;
|
||||
/** Kitty image size limit in bytes (default 20000000 bytes). */
|
||||
kittySizeLimit?: number;
|
||||
}
|
||||
|
||||
export class ImageAddon implements ITerminalAddon {
|
||||
|
||||
@@ -3,7 +3,6 @@ node_modules/
|
||||
coverage/
|
||||
|
||||
lib/
|
||||
fonts/
|
||||
|
||||
.env
|
||||
.vscode/
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018
|
||||
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
The code that analyzes font ligatures is forked from https://github.com/princjef/font-ligatures with this license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Jeffrey Principe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const axios = require('axios').default;
|
||||
const mkdirp = require('mkdirp');
|
||||
const yauzl = require('yauzl');
|
||||
|
||||
const urls = {
|
||||
fira: 'https://github.com/tonsky/FiraCode/raw/d42e7276fa925e5f82748f3ec9ea429736611b48/distr/otf/FiraCode-Regular.otf',
|
||||
iosevka: 'https://github.com/be5invis/Iosevka/releases/download/v1.14.3/01-iosevka-1.14.3.zip'
|
||||
};
|
||||
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const fontsFolder = path.join(__dirname, '../fonts');
|
||||
|
||||
async function download() {
|
||||
await mkdirp(fontsFolder);
|
||||
|
||||
try {
|
||||
await downloadFiraCode();
|
||||
await downloadIosevka();
|
||||
console.log('Loaded all fonts for testing')
|
||||
} catch (e) {
|
||||
console.warn('Fonts failed to download, ligature tests will not work', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFiraCode() {
|
||||
const file = path.join(fontsFolder, 'firaCode.otf');
|
||||
if (await util.promisify(fs.exists)(file)) {
|
||||
console.log('Fira Code already loaded');
|
||||
} else {
|
||||
console.log('Downloading Fira Code...');
|
||||
await writeFile(
|
||||
file,
|
||||
(await axios.get(urls.fira, { responseType: 'arraybuffer' })).data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadIosevka() {
|
||||
const file = path.join(fontsFolder, 'iosevka.ttf');
|
||||
if (await util.promisify(fs.exists)(file)) {
|
||||
console.log('Iosevka already loaded');
|
||||
} else {
|
||||
console.log('Downloading Iosevka...');
|
||||
const iosevkaContents = (await axios.get(urls.iosevka, { responseType: 'arraybuffer' })).data;
|
||||
const iosevkaZipfile = await util.promisify(yauzl.fromBuffer)(iosevkaContents);
|
||||
await new Promise((resolve, reject) => {
|
||||
iosevkaZipfile.on('entry', entry => {
|
||||
if (entry.fileName === 'ttf/iosevka-regular.ttf') {
|
||||
iosevkaZipfile.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const writeStream = fs.createWriteStream(file);
|
||||
stream.pipe(writeStream);
|
||||
writeStream.on('close', () => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
download();
|
||||
|
||||
process.on('unhandledRejection', e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
-661
@@ -1,661 +0,0 @@
|
||||
{
|
||||
"name": "@xterm/addon-ligatures",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@xterm/addon-ligatures",
|
||||
"version": "0.9.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.1.0",
|
||||
"font-ligatures": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sinon": "^5.0.1",
|
||||
"axios": "^1.6.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"sinon": "6.3.5",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/commons": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz",
|
||||
"integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/formatio": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz",
|
||||
"integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^1",
|
||||
"@sinonjs/samsam": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/formatio/node_modules/@sinonjs/samsam": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz",
|
||||
"integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^1.3.0",
|
||||
"array-from": "^2.1.1",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/samsam": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz",
|
||||
"integrity": "sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@sinonjs/text-encoding": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
|
||||
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
|
||||
"dev": true,
|
||||
"license": "(Unlicense OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/@types/sinon": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.1.tgz",
|
||||
"integrity": "sha512-yxzBCIjE3lp9lYjfBbIK/LRCoXgCLLbIIBIje7eNCcUIIR2CZZtyX5uto2hVoMSMqLrsRrT6mwwUEd0yFgOwpA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-from": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
|
||||
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/font-finder": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz",
|
||||
"integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-system-fonts": "^2.0.0",
|
||||
"promise-stream-reader": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/font-ligatures": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz",
|
||||
"integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.0.3",
|
||||
"lru-cache": "^6.0.0",
|
||||
"opentype.js": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-system-fonts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.0.tgz",
|
||||
"integrity": "sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/just-extend": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
|
||||
"integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lolex": {
|
||||
"version": "2.7.5",
|
||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
|
||||
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nise": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz",
|
||||
"integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/formatio": "^3.2.1",
|
||||
"@sinonjs/text-encoding": "^0.7.1",
|
||||
"just-extend": "^4.0.2",
|
||||
"lolex": "^5.0.1",
|
||||
"path-to-regexp": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nise/node_modules/lolex": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz",
|
||||
"integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/opentype.js": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz",
|
||||
"integrity": "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tiny-inflate": "^1.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"ot": "bin/ot"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/promise-stream-reader": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz",
|
||||
"integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sinon": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz",
|
||||
"integrity": "sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==",
|
||||
"deprecated": "16.1.1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^1.0.2",
|
||||
"@sinonjs/formatio": "^3.0.0",
|
||||
"@sinonjs/samsam": "^2.1.2",
|
||||
"diff": "^3.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lolex": "^2.7.5",
|
||||
"nise": "^1.4.5",
|
||||
"supports-color": "^5.5.0",
|
||||
"type-detect": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz",
|
||||
"integrity": "sha512-fEndoHfuiSlXtvG0EA1gB4uAdy2dbQ3WfOtQw1/Y8NQ9rxtGbGkENwfGmN0wK7OkxDz1lgc5Ei2LQ0oxKDUcVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "node bin/download-fonts.js",
|
||||
"build": "tsc -p src",
|
||||
"watch": "tsc -w -p src",
|
||||
"prepackage": "npm run build",
|
||||
@@ -32,14 +31,15 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.1.0",
|
||||
"font-ligatures": "^1.4.1"
|
||||
"lru-cache": "^6.0.0",
|
||||
"opentype.js": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sinon": "^5.0.1",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/opentype.js": "^0.7.0",
|
||||
"axios": "^1.6.0",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"sinon": "6.3.5",
|
||||
"yauzl": "^2.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ITerminalAddon {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
export class LigaturesAddon implements ITerminalAddon, ILigaturesApi {
|
||||
private readonly _fallbackLigatures: string[];
|
||||
private readonly _fontFeatureSettings?: string;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
|
||||
constructor(options?: Partial<ILigatureOptions>) {
|
||||
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
|
||||
this._fallbackLigatures = (options?.fallbackLigatures || [
|
||||
this._fallbackLigatures = (options?.fallbackLigatures ?? [
|
||||
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
||||
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Font, loadBuffer } from 'font-ligatures';
|
||||
import { Font, loadBuffer } from './fontLigatures/index';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
@@ -42,11 +42,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
if (status && status.state !== 'granted') {
|
||||
throw new Error('Permission to access local fonts not granted.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// A `TypeError` indicates the 'local-fonts'
|
||||
// permission is not yet implemented, so
|
||||
// only `throw` if this is _not_ the problem.
|
||||
if (err.name !== 'TypeError') {
|
||||
if (err instanceof Error && err.name !== 'TypeError') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,10 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||
@@ -76,13 +78,13 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fontsPromise) {
|
||||
fontsPromise = Promise.resolve({});
|
||||
}
|
||||
fontsPromise ??= Promise.resolve({});
|
||||
}
|
||||
|
||||
const fonts = await fontsPromise;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types';
|
||||
|
||||
export default function flatten(tree: ILookupTree, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry> = new Map()): IFlattenedLookupTree {
|
||||
const result: IFlattenedLookupTree = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
result[glyphId] = flattenEntry(entry, visited);
|
||||
}
|
||||
|
||||
for (const { range, entry } of tree.range) {
|
||||
const flattened = flattenEntry(entry, visited);
|
||||
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
|
||||
result[glyphId] = flattened;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function flattenEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry>): IFlattenedLookupTreeEntry {
|
||||
if (visited.has(entry)) {
|
||||
return visited.get(entry)!;
|
||||
}
|
||||
|
||||
const result: IFlattenedLookupTreeEntry = {};
|
||||
visited.set(entry, result);
|
||||
|
||||
if (entry.forward) {
|
||||
result.forward = flatten(entry.forward, visited);
|
||||
}
|
||||
|
||||
if (entry.reverse) {
|
||||
result.reverse = flatten(entry.reverse, visited);
|
||||
}
|
||||
|
||||
if (entry.lookup) {
|
||||
result.lookup = entry.lookup;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { assert } from 'chai';
|
||||
import { loadBuffer } from './index';
|
||||
|
||||
interface IFont {
|
||||
findLigatures(text: string): { outputGlyphs: number[], contextRanges: [number, number][] };
|
||||
findLigatureRanges(text: string): [number, number][];
|
||||
}
|
||||
|
||||
interface ITestCase {
|
||||
font: string;
|
||||
input: string;
|
||||
glyphs: number[];
|
||||
ranges: [number, number][];
|
||||
}
|
||||
|
||||
const fira = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Fira Code', input, glyphs, ranges });
|
||||
|
||||
const iosevka = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Iosevka', input, glyphs, ranges });
|
||||
|
||||
const monoid = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Monoid', input, glyphs, ranges });
|
||||
|
||||
const ubuntu = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Ubuntu Mono', input, glyphs, ranges });
|
||||
|
||||
const firaCases: ITestCase[] = [
|
||||
fira('abc', [133, 145, 146], []),
|
||||
fira('.=', [1614, 1081], [[0, 2]]),
|
||||
fira('..=', [1614, 1614, 1083], [[0, 3]]),
|
||||
fira('.-', [1614, 1080], [[0, 2]]),
|
||||
fira(':=', [1614, 1055], [[0, 2]]),
|
||||
fira('=:=', [1614, 1614, 1483], [[0, 3]]),
|
||||
fira('=!=', [1614, 1614, 1484], [[0, 3]]),
|
||||
fira('__', [1614, 1099], [[0, 2]]),
|
||||
fira('==', [1614, 1485], [[0, 2]]),
|
||||
fira('!=', [1614, 1058], [[0, 2]]),
|
||||
fira('===', [1614, 1614, 1486], [[0, 3]]),
|
||||
fira('!==', [1614, 1614, 1059], [[0, 3]]),
|
||||
fira('=/=', [1614, 1614, 1491], [[0, 3]]),
|
||||
fira('<-<', [1614, 1614, 1513], [[0, 3]]),
|
||||
fira('<<-', [1614, 1614, 1522], [[0, 3]]),
|
||||
fira('<--', [1614, 1614, 1511], [[0, 3]]),
|
||||
fira('<-', [1614, 1510], [[0, 2]]),
|
||||
fira('<->', [1614, 1614, 1512], [[0, 3]]),
|
||||
fira('->', [1614, 1064], [[0, 2]]),
|
||||
fira('-->', [1614, 1614, 1063], [[0, 3]]),
|
||||
fira('->>', [1614, 1614, 1065], [[0, 3]]),
|
||||
fira('>->', [1614, 1614, 1493], [[0, 3]]),
|
||||
fira('<=<', [1614, 1614, 1519], [[0, 3]]),
|
||||
fira('<<=', [1614, 1614, 1523], [[0, 3]]),
|
||||
fira('<==', [1614, 1614, 1517], [[0, 3]]),
|
||||
fira('<=>', [1614, 1614, 1518], [[0, 3]]),
|
||||
fira('=>', [1614, 1488], [[0, 2]]),
|
||||
fira('==>', [1614, 1614, 1487], [[0, 3]]),
|
||||
fira('=>>', [1614, 1614, 1489], [[0, 3]]),
|
||||
fira('>=>', [1614, 1614, 1495], [[0, 3]]),
|
||||
fira('>>=', [1614, 1614, 1498], [[0, 3]]),
|
||||
fira('>>-', [1614, 1614, 1497], [[0, 3]]),
|
||||
fira('>-', [1614, 1492], [[0, 2]]),
|
||||
fira('<~>', [1614, 1614, 1526], [[0, 3]]),
|
||||
fira('-<', [1614, 1066], [[0, 2]]),
|
||||
fira('-<<', [1614, 1614, 1067], [[0, 3]]),
|
||||
fira('=<<', [1614, 1614, 1490], [[0, 3]]),
|
||||
fira('<~~', [1614, 1614, 1527], [[0, 3]]),
|
||||
fira('<~', [1614, 1525], [[0, 2]]),
|
||||
fira('~~', [1614, 1534], [[0, 2]]),
|
||||
fira('~>', [1614, 1533], [[0, 2]]),
|
||||
fira('~~>', [1614, 1614, 1535], [[0, 3]]),
|
||||
fira('<<<', [1614, 1614, 1524], [[0, 3]]),
|
||||
fira('<<', [1614, 1521], [[0, 2]]),
|
||||
fira('<=', [1614, 1516], [[0, 2]]),
|
||||
fira('<>', [1614, 1520], [[0, 2]]),
|
||||
fira('>=', [1614, 1494], [[0, 2]]),
|
||||
fira('>>', [1614, 1496], [[0, 2]]),
|
||||
fira('>>>', [1614, 1614, 1499], [[0, 3]]),
|
||||
fira('{.', [1001, 977], [[0, 2]]),
|
||||
fira('{|', [1614, 1049], [[0, 2]]),
|
||||
fira('[|', [1614, 1050], [[0, 2]]),
|
||||
fira('<:', [1614, 1506], [[0, 2]]),
|
||||
fira(':>', [1614, 1056], [[0, 2]]),
|
||||
fira('|]', [1614, 1474], [[0, 2]]),
|
||||
fira('|}', [1614, 1473], [[0, 2]]),
|
||||
fira('.}', [977, 1002], [[0, 2]]),
|
||||
fira('<|||', [1614, 1614, 1614, 1504], [[0, 4]]),
|
||||
fira('<||', [1614, 1614, 1503], [[0, 3]]),
|
||||
fira('<|', [1614, 1502], [[0, 2]]),
|
||||
fira('<|>', [1614, 1614, 1505], [[0, 3]]),
|
||||
fira('|>', [1614, 1477], [[0, 2]]),
|
||||
fira('||>', [1614, 1614, 1472], [[0, 3]]),
|
||||
fira('|||>', [1614, 1614, 1614, 1470], [[0, 4]]),
|
||||
fira('<$', [1614, 1507], [[0, 2]]),
|
||||
fira('<$>', [1614, 1614, 1508], [[0, 3]]),
|
||||
fira('$>', [1614, 1479], [[0, 2]]),
|
||||
fira('<+', [1614, 1514], [[0, 2]]),
|
||||
fira('<+>', [1614, 1614, 1515], [[0, 3]]),
|
||||
fira('+>', [1614, 1482], [[0, 2]]),
|
||||
fira('<*', [1614, 1500], [[0, 2]]),
|
||||
fira('<*>', [1614, 1614, 1501], [[0, 3]]),
|
||||
fira('*>', [1614, 1047], [[0, 2]]),
|
||||
fira('/*', [1614, 1092], [[0, 2]]),
|
||||
fira('*/', [1614, 1048], [[0, 2]]),
|
||||
fira('///', [1614, 1614, 1097], [[0, 3]]),
|
||||
fira('//', [1614, 1096], [[0, 2]]),
|
||||
fira('</', [1614, 1528], [[0, 2]]),
|
||||
fira('<!--', [1614, 1614, 1614, 1509], [[0, 4]]),
|
||||
fira('</>', [1614, 1614, 1529], [[0, 3]]),
|
||||
fira('/>', [1614, 1095], [[0, 2]]),
|
||||
fira('0xff', [895, 270, 166, 166], [[0, 3]]),
|
||||
fira('10x10', [896, 895, 270, 896, 895], [[1, 4]]),
|
||||
fira('9:45', [904, 998, 899, 900], [[0, 2]]),
|
||||
fira('[:]', [1003, 998, 1004], [[0, 2]]),
|
||||
fira(';;', [1614, 1091], [[0, 2]]),
|
||||
fira('::', [1614, 1052], [[0, 2]]),
|
||||
fira(':::', [1614, 1614, 1053], [[0, 3]]),
|
||||
fira('..', [1614, 1082], [[0, 2]]),
|
||||
fira('...', [1614, 1614, 1085], [[0, 3]]),
|
||||
fira('..<', [1614, 1614, 1084], [[0, 3]]),
|
||||
fira('!!', [1614, 1057], [[0, 2]]),
|
||||
fira('??', [1614, 1090], [[0, 2]]),
|
||||
fira('%%', [1614, 1536], [[0, 2]]),
|
||||
fira('&&', [1614, 1468], [[0, 2]]),
|
||||
fira('||', [1614, 1469], [[0, 2]]),
|
||||
fira('?.', [1614, 1089], [[0, 2]]),
|
||||
fira('?:', [1614, 1087], [[0, 2]]),
|
||||
fira('++', [1614, 1480], [[0, 2]]),
|
||||
fira('+++', [1614, 1614, 1481], [[0, 3]]),
|
||||
fira('--', [1614, 1061], [[0, 2]]),
|
||||
fira('---', [1614, 1614, 1062], [[0, 3]]),
|
||||
fira('**', [1614, 1045], [[0, 2]]),
|
||||
fira('***', [1614, 1614, 1046], [[0, 3]]),
|
||||
fira('~=', [1614, 1532], [[0, 2]]),
|
||||
fira('~-', [1614, 1531], [[0, 2]]),
|
||||
fira('www', [1614, 1614, 271], [[0, 3]]),
|
||||
fira('-~', [1614, 1068], [[0, 2]]),
|
||||
fira('~@', [1614, 1530], [[0, 2]]),
|
||||
fira('^=', [1614, 1478], [[0, 2]]),
|
||||
fira('?=', [1614, 1088], [[0, 2]]),
|
||||
fira('/=', [1614, 1093], [[0, 2]]),
|
||||
fira('/==', [1614, 1614, 1094], [[0, 3]]),
|
||||
fira('-|', [1614, 1060], [[0, 2]]),
|
||||
fira('_|_', [1614, 1614, 1098], [[0, 3]]),
|
||||
fira('|-', [1614, 1475], [[0, 2]]),
|
||||
fira('|=', [1614, 1476], [[0, 2]]),
|
||||
fira('||=', [1614, 1614, 1471], [[0, 3]]),
|
||||
fira('#!', [1614, 1071], [[0, 2]]),
|
||||
fira('#=', [1614, 1075], [[0, 2]]),
|
||||
fira('##', [1614, 1072], [[0, 2]]),
|
||||
fira('###', [1614, 1614, 1073], [[0, 3]]),
|
||||
fira('####', [1614, 1614, 1614, 1074], [[0, 4]]),
|
||||
fira('#{', [1614, 1069], [[0, 2]]),
|
||||
fira('#[', [1614, 1070], [[0, 2]]),
|
||||
fira(']#', [1614, 1051], [[0, 2]]),
|
||||
fira('#(', [1614, 1076], [[0, 2]]),
|
||||
fira('#?', [1614, 1077], [[0, 2]]),
|
||||
fira('#_', [1614, 1078], [[0, 2]]),
|
||||
fira('#_(', [1614, 1614, 1079], [[0, 3]]),
|
||||
fira('::=', [1614, 1614, 1054], [[0, 3]]),
|
||||
fira('.?', [1614, 1086], [[0, 2]]),
|
||||
fira('===>', [1614, 1614, 1486, 1148], [[0, 4]])
|
||||
];
|
||||
|
||||
const iosevkaCases: ITestCase[] = [
|
||||
iosevka('<-', [31, 3127], [[0, 2]]),
|
||||
iosevka('<--', [31, 3129, 3139], [[0, 3]]),
|
||||
iosevka('<---', [31, 3129, 3150, 3139], [[0, 4]]),
|
||||
iosevka('<-----', [31, 3129, 3150, 3139, 3151, 3151], [[0, 6]]),
|
||||
iosevka('->', [3126, 33], [[0, 2]]),
|
||||
iosevka('-->', [3140, 3128, 33], [[0, 3]]),
|
||||
iosevka('--->', [3140, 3150, 3128, 33], [[0, 4]]),
|
||||
iosevka('----->', [3153, 3153, 3140, 3150, 3128, 33], [[0, 6]]),
|
||||
iosevka('<->', [31, 3149, 33], [[0, 3]]),
|
||||
iosevka('<-->', [31, 3129, 3128, 33], [[0, 4]]),
|
||||
iosevka('<--->', [31, 3129, 3150, 3128, 33], [[0, 5]]),
|
||||
iosevka('<----->', [31, 3129, 3150, 3150, 3150, 3128, 33], [[0, 7]]),
|
||||
iosevka('<=', [3094, 3095], [[0, 2]]),
|
||||
iosevka('<==', [31, 3158, 3168], [[0, 3]]),
|
||||
iosevka('<===', [31, 3158, 3179, 3168], [[0, 4]]),
|
||||
iosevka('<=====', [31, 3158, 3179, 3168, 3180, 3180], [[0, 6]]),
|
||||
iosevka('=>', [3155, 33], [[0, 2]]),
|
||||
iosevka('==>', [3169, 3157, 33], [[0, 3]]),
|
||||
iosevka('===>', [3169, 3179, 3157, 33], [[0, 4]]),
|
||||
iosevka('=====>', [3182, 3182, 3169, 3179, 3157, 33], [[0, 6]]),
|
||||
iosevka('<=>', [31, 3178, 33], [[0, 3]]),
|
||||
iosevka('<==>', [31, 3158, 3157, 33], [[0, 4]]),
|
||||
iosevka('<===>', [31, 3158, 3179, 3157, 33], [[0, 5]]),
|
||||
iosevka('<=====>', [31, 3158, 3179, 3179, 3179, 3157, 33], [[0, 7]]),
|
||||
iosevka('<!--', [31, 3184, 3130, 3139], [[0, 4]]),
|
||||
iosevka('<!---', [31, 3184, 3130, 3150, 3139], [[0, 5]]),
|
||||
iosevka('<!-----', [31, 3184, 3130, 3150, 3139, 3151, 3151], [[0, 7]]),
|
||||
iosevka('a:b', [68, 29, 69], []),
|
||||
iosevka('a::b', [68, 3012, 3013, 69], [[1, 3]]),
|
||||
iosevka('a:::b', [68, 3012, 3010, 3013, 69], [[1, 4]]),
|
||||
iosevka(':=', [3011, 32], [[0, 2]]),
|
||||
iosevka(':-', [3011, 16], [[0, 2]]),
|
||||
iosevka(':+', [3011, 14], [[0, 2]]),
|
||||
iosevka('=:', [32, 3011], [[0, 2]]),
|
||||
iosevka('-:', [16, 3011], [[0, 2]]),
|
||||
iosevka('+:', [14, 3011], [[0, 2]]),
|
||||
iosevka('<*', [31, 3023], [[0, 2]]),
|
||||
iosevka('*>', [3023, 33], [[0, 2]]),
|
||||
iosevka('<*>', [31, 3023, 33], [[1, 3]]),
|
||||
iosevka('<**>', [31, 3023, 3023, 33], [[1, 4]]),
|
||||
iosevka('<****>', [31, 3023, 3023, 3023, 3023, 33], [[0, 3], [3, 6]]),
|
||||
iosevka('==', [3083, 3084], [[0, 2]]),
|
||||
iosevka('!=', [3098, 3084], [[0, 2]]),
|
||||
iosevka('===', [3083, 3085, 3084], [[0, 3]]),
|
||||
iosevka('!==', [3099, 3085, 3084], [[0, 3]]),
|
||||
iosevka('====', [3083, 3085, 3085, 3084], [[0, 4]]),
|
||||
iosevka('!===', [3100, 3085, 3085, 3084], [[0, 4]])
|
||||
];
|
||||
|
||||
const monoidCases: ITestCase[] = [
|
||||
monoid('<!--', [775, 775, 775, 643], [[0, 4]]),
|
||||
monoid('-->', [779, 779, 628], [[0, 3]]),
|
||||
monoid('<--', [776, 776, 627], [[0, 3]]),
|
||||
monoid('->>', [780, 780, 626], [[0, 3]]),
|
||||
monoid('<<-', [777, 777, 625], [[0, 3]]),
|
||||
monoid('->', [781, 623], [[0, 2]]),
|
||||
monoid('<-', [778, 624], [[0, 2]]),
|
||||
monoid('=>', [793, 666], [[0, 2]]),
|
||||
monoid('<=>', [785, 785, 760], [[0, 3]]),
|
||||
monoid('<==>', [786, 786, 786, 771], [[0, 4]]),
|
||||
monoid('==>', [787, 787, 672], [[0, 3]]),
|
||||
monoid('<==', [788, 788, 671], [[0, 3]]),
|
||||
monoid('>>=', [791, 791, 758], [[0, 3]]),
|
||||
monoid('=<<', [792, 792, 759], [[0, 3]]),
|
||||
monoid('--', [667, 667], [[0, 2]]),
|
||||
monoid(':=', [29, 761], [[0, 2]]),
|
||||
monoid('=:=', [789, 789, 665], [[0, 3]]),
|
||||
monoid('==', [794, 641], [[0, 2]]),
|
||||
monoid('!==', [782, 782, 646], [[0, 3]]),
|
||||
monoid('!=', [783, 629], [[0, 2]]),
|
||||
monoid('<=', [790, 630], [[0, 2]]),
|
||||
monoid('>=', [792, 631], [[0, 2]]),
|
||||
monoid('//', [621, 664], [[0, 2]]),
|
||||
monoid('/**', [18, 753, 753], [[0, 3]]),
|
||||
monoid('/*', [18, 753], [[0, 2]]),
|
||||
monoid('*/', [754, 18], [[0, 2]]),
|
||||
monoid('&&', [633, 775], [[0, 2]]),
|
||||
monoid('.&', [17, 755], [[0, 2]]),
|
||||
monoid('||', [634, 635], [[0, 2]]),
|
||||
monoid('!!', [769, 770], [[0, 2]]),
|
||||
monoid('::', [772, 773], [[0, 2]]),
|
||||
monoid('>>', [637, 638], [[0, 2]]),
|
||||
monoid('<<', [639, 640], [[0, 2]]),
|
||||
monoid('¯\\_(ツ)_/¯', [113, 765, 66, 767, 613, 768, 66, 766, 113], [[0, 3], [3, 6], [6, 9]]),
|
||||
monoid('__', [763, 764], [[0, 2]])
|
||||
];
|
||||
|
||||
const ubuntuCases: ITestCase[] = [
|
||||
ubuntu('==>', [32, 32, 33], [])
|
||||
];
|
||||
|
||||
const fontPaths: Record<string, string> = {
|
||||
'Fira Code': path.join(__dirname, '../../fonts/FiraCode-Regular.otf'),
|
||||
'Iosevka': path.join(__dirname, '../../fonts/iosevka-regular.ttf'),
|
||||
'Monoid': path.join(__dirname, '../../fonts/Monoid-Regular.ttf'),
|
||||
'Ubuntu Mono': path.join(__dirname, '../../fonts/UbuntuMono-Regular.ttf')
|
||||
};
|
||||
|
||||
const fontCache: Map<string, IFont> = new Map();
|
||||
|
||||
function loadFont(fontName: string): IFont {
|
||||
let font = fontCache.get(fontName);
|
||||
if (!font) {
|
||||
const fontPath = fontPaths[fontName];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
fontCache.set(fontName, font);
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
describe('addon-ligatures - index', () => {
|
||||
describe('findLigatures', () => {
|
||||
for (const { font: fontName, input, glyphs, ranges } of [...firaCases, ...iosevkaCases, ...monoidCases, ...ubuntuCases]) {
|
||||
it(`${fontName}: '${input}'`, () => {
|
||||
const font = loadFont(fontName);
|
||||
const result = font.findLigatures(input);
|
||||
assert.deepEqual(result.outputGlyphs, glyphs);
|
||||
assert.deepEqual(result.contextRanges, ranges);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('findLigatureRanges', () => {
|
||||
for (const { font: fontName, input, ranges } of [...firaCases, ...iosevkaCases, ...monoidCases, ...ubuntuCases]) {
|
||||
it(`${fontName}: '${input}'`, () => {
|
||||
const font = loadFont(fontName);
|
||||
const result = font.findLigatureRanges(input);
|
||||
assert.deepEqual(result, ranges);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('caching', () => {
|
||||
it('findLigatures caches successive calls correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatures('in --> out');
|
||||
const result2 = font.findLigatures('in --> out');
|
||||
assert.deepEqual(result1, result2);
|
||||
});
|
||||
|
||||
it('findLigatureRanges caches successive calls correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatureRanges('in --> out');
|
||||
const result2 = font.findLigatureRanges('in --> out');
|
||||
assert.deepEqual(result1, result2);
|
||||
});
|
||||
|
||||
it('caches calls to findLigatures after findLigatureRanges correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
|
||||
const uncached = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
const uncachedResult1 = uncached.findLigatureRanges('in --> out');
|
||||
const uncachedResult2 = uncached.findLigatures('in --> out');
|
||||
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatureRanges('in --> out');
|
||||
const result2 = font.findLigatures('in --> out');
|
||||
|
||||
assert.deepEqual(result1, uncachedResult1);
|
||||
assert.deepEqual(result2, uncachedResult2);
|
||||
assert.deepEqual(result1, result2.contextRanges);
|
||||
});
|
||||
|
||||
it('caches calls to findLigatureRanges after findLigatures correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
|
||||
const uncached = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
const uncachedResult1 = uncached.findLigatures('in --> out');
|
||||
const uncachedResult2 = uncached.findLigatureRanges('in --> out');
|
||||
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatures('in --> out');
|
||||
const result2 = font.findLigatureRanges('in --> out');
|
||||
|
||||
assert.deepEqual(result1, uncachedResult1);
|
||||
assert.deepEqual(result2, uncachedResult2);
|
||||
assert.deepEqual(result1.contextRanges, result2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import * as opentype from 'opentype.js';
|
||||
import LRUCache = require('lru-cache');
|
||||
|
||||
import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types';
|
||||
import mergeTrees from './merge';
|
||||
import walkTree from './walk';
|
||||
import mergeRange from './mergeRange';
|
||||
|
||||
import buildTreeGsubType6Format1 from './processors/6-1';
|
||||
import buildTreeGsubType6Format2 from './processors/6-2';
|
||||
import buildTreeGsubType6Format3 from './processors/6-3';
|
||||
import buildTreeGsubType8Format1 from './processors/8-1';
|
||||
import flatten from './flatten';
|
||||
|
||||
class FontImpl implements IFont {
|
||||
private _font: opentype.Font;
|
||||
private _lookupTrees: { tree: IFlattenedLookupTree, processForward: boolean }[] = [];
|
||||
private _glyphLookups: { [glyphId: string]: number[] } = {};
|
||||
private _cache?: LRUCache<string, ILigatureData | [number, number][]>;
|
||||
|
||||
constructor(font: opentype.Font, options: Required<IOptions>) {
|
||||
this._font = font;
|
||||
|
||||
if (options.cacheSize > 0) {
|
||||
this._cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
|
||||
});
|
||||
}
|
||||
|
||||
const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter((f: { tag: string }) => f.tag === 'calt') || [];
|
||||
const lookupIndices: number[] = caltFeatures
|
||||
.reduce((acc: number[], val: { feature: { lookupListIndexes: number[] } }) => [...acc, ...val.feature.lookupListIndexes], []);
|
||||
|
||||
const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || [];
|
||||
const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i));
|
||||
|
||||
for (const [index, lookup] of lookupGroups.entries()) {
|
||||
const trees: ILookupTree[] = [];
|
||||
switch (lookup.lookupType) {
|
||||
case 6:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
switch (table.substFormat) {
|
||||
case 1:
|
||||
trees.push(buildTreeGsubType6Format1(table, allLookups, index));
|
||||
break;
|
||||
case 2:
|
||||
trees.push(buildTreeGsubType6Format2(table, allLookups, index));
|
||||
break;
|
||||
case 3:
|
||||
trees.push(buildTreeGsubType6Format3(table, allLookups, index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
trees.push(buildTreeGsubType8Format1(table, index));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const tree = flatten(mergeTrees(trees));
|
||||
|
||||
this._lookupTrees.push({
|
||||
tree,
|
||||
processForward: lookup.lookupType !== 8
|
||||
});
|
||||
|
||||
for (const glyphId of Object.keys(tree)) {
|
||||
if (!this._glyphLookups[glyphId]) {
|
||||
this._glyphLookups[glyphId] = [];
|
||||
}
|
||||
|
||||
this._glyphLookups[glyphId].push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public findLigatures(text: string): ILigatureData {
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached && !Array.isArray(cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
// If there are no lookup groups, there's no point looking for
|
||||
// replacements. This gives us a minor performance boost for fonts with
|
||||
// no ligatures
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: glyphIds,
|
||||
contextRanges: []
|
||||
};
|
||||
}
|
||||
|
||||
const result = this._findInternal(glyphIds.slice());
|
||||
const finalResult: ILigatureData = {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: result.sequence,
|
||||
contextRanges: result.ranges
|
||||
};
|
||||
if (this._cache) {
|
||||
this._cache.set(text, finalResult);
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
public findLigatureRanges(text: string): [number, number][] {
|
||||
// Short circuit the process if there are no possible ligatures in the
|
||||
// font
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached) {
|
||||
return Array.isArray(cached) ? cached : cached.contextRanges;
|
||||
}
|
||||
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
const result = this._findInternal(glyphIds);
|
||||
if (this._cache) {
|
||||
this._cache.set(text, result.ranges);
|
||||
}
|
||||
|
||||
return result.ranges;
|
||||
}
|
||||
|
||||
private _findInternal(sequence: number[]): { sequence: number[], ranges: [number, number][] } {
|
||||
const ranges: [number, number][] = [];
|
||||
|
||||
let nextLookup = this._getNextLookup(sequence, 0);
|
||||
while (nextLookup.index !== null) {
|
||||
const lookup = this._lookupTrees[nextLookup.index];
|
||||
if (lookup.processForward) {
|
||||
let lastGlyphIndex = nextLookup.last;
|
||||
for (let i = nextLookup.first; i < lastGlyphIndex; i++) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
|
||||
// Substitutions can end up extending the search range
|
||||
if (i + result.length >= lastGlyphIndex) {
|
||||
lastGlyphIndex = i + result.length + 1;
|
||||
}
|
||||
|
||||
i += result.length - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't need to do the lastGlyphIndex tracking here because
|
||||
// reverse processing isn't allowed to replace more than one
|
||||
// character at a time.
|
||||
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
|
||||
i -= result.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextLookup = this._getNextLookup(sequence, nextLookup.index + 1);
|
||||
}
|
||||
|
||||
return { sequence, ranges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lookup and glyph range for the first lookup that might
|
||||
* contain a match.
|
||||
*
|
||||
* @param sequence Input glyph sequence
|
||||
* @param start The first input to try
|
||||
*/
|
||||
private _getNextLookup(sequence: number[], start: number): { index: number | null, first: number, last: number } {
|
||||
const result: { index: number | null, first: number, last: number } = {
|
||||
index: null,
|
||||
first: Infinity,
|
||||
last: -1
|
||||
};
|
||||
|
||||
// Loop through each glyph and find the first valid lookup for it
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const lookups = this._glyphLookups[sequence[i]];
|
||||
if (!lookups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < lookups.length; j++) {
|
||||
const lookupIndex = lookups[j];
|
||||
if (lookupIndex >= start) {
|
||||
// Update the lookup information if it's the one we're
|
||||
// storing or earlier than it.
|
||||
if (result.index === null || lookupIndex <= result.index) {
|
||||
result.index = lookupIndex;
|
||||
|
||||
if (result.first > i) {
|
||||
result.first = i;
|
||||
}
|
||||
|
||||
result.last = i + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the font from it's binary data. The returned value can be used to find
|
||||
* ligatures for the font.
|
||||
*
|
||||
* @param buffer ArrayBuffer of the font to load
|
||||
*/
|
||||
export function loadBuffer(buffer: ArrayBuffer, options?: IOptions): IFont {
|
||||
const font = opentype.parse(buffer);
|
||||
return new FontImpl(font, {
|
||||
cacheSize: 0,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
export { IFont as Font, ILigatureData as LigatureData, IOptions as Options };
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import mergeTrees from './merge';
|
||||
|
||||
interface ILookupResult {
|
||||
contextRange: [number, number];
|
||||
index: number;
|
||||
subIndex: number;
|
||||
length: number;
|
||||
substitutions: number[];
|
||||
}
|
||||
|
||||
function lookup(substitutionGlyph: number, index?: number, subIndex?: number): ILookupResult {
|
||||
return {
|
||||
contextRange: [0, 1],
|
||||
index: index || 0,
|
||||
subIndex: subIndex || 0,
|
||||
length: 1,
|
||||
substitutions: [substitutionGlyph]
|
||||
};
|
||||
}
|
||||
|
||||
describe('addon-ligatures - merge', () => {
|
||||
describe('mergeTrees', () => {
|
||||
it('combines disjoint trees', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2) },
|
||||
range: [2, 4]
|
||||
}]
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'5': { lookup: lookup(3) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(4) },
|
||||
range: [8, 10]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'1': { lookup: lookup(1) },
|
||||
'5': { lookup: lookup(3) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2) },
|
||||
range: [2, 4]
|
||||
}, {
|
||||
entry: { lookup: lookup(4) },
|
||||
range: [8, 10]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('merges matching individual glyphs', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1, 1) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(2, 0) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(3, 2) }
|
||||
},
|
||||
range: []
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'1': { lookup: lookup(2, 0) }
|
||||
},
|
||||
range: []
|
||||
});
|
||||
});
|
||||
|
||||
it('merges range glyphs overlapping individual glyphs', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [0, 4]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'0': { lookup: lookup(2, 1) },
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [2, 4]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('merges individual glyphs overlapping range glyphs', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [0, 4]
|
||||
}]
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: []
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'0': { lookup: lookup(2, 1) },
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [2, 4]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('merges multiple overlapping ranges', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(1, 2) },
|
||||
range: [0, 3]
|
||||
}, {
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [6, 12]
|
||||
}, {
|
||||
entry: { lookup: lookup(5, 3) },
|
||||
range: [15, 20]
|
||||
}, {
|
||||
entry: { lookup: lookup(7, 4) },
|
||||
range: [20, 22]
|
||||
}]
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(3, 0) },
|
||||
range: [2, 8]
|
||||
}, {
|
||||
entry: { lookup: lookup(4, 0) },
|
||||
range: [10, 13]
|
||||
}, {
|
||||
entry: { lookup: lookup(6, 0) },
|
||||
range: [16, 21]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'2': { lookup: lookup(3, 0) },
|
||||
'12': { lookup: lookup(4, 0) },
|
||||
'15': { lookup: lookup(5, 3) },
|
||||
'20': { lookup: lookup(6, 0) },
|
||||
'21': { lookup: lookup(7, 4) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(1, 2) },
|
||||
range: [0, 2]
|
||||
}, {
|
||||
entry: { lookup: lookup(3, 0) },
|
||||
range: [6, 8]
|
||||
}, {
|
||||
entry: { lookup: lookup(3, 0) },
|
||||
range: [3, 6]
|
||||
}, {
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [8, 10]
|
||||
}, {
|
||||
entry: { lookup: lookup(4, 0) },
|
||||
range: [10, 12]
|
||||
}, {
|
||||
entry: { lookup: lookup(6, 0) },
|
||||
range: [16, 20]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,393 @@
|
||||
import { ILookupTree, ILookupTreeEntry } from './types';
|
||||
|
||||
/**
|
||||
* Merges the provided trees into a single lookup tree. When conflicting lookups
|
||||
* are encountered between two trees, the one with the lower index, then the
|
||||
* lower subindex is chosen.
|
||||
*
|
||||
* @param trees Array of trees to merge. Entries in earlier trees are favored
|
||||
* over those in later trees when there is a choice.
|
||||
*/
|
||||
export default function mergeTrees(trees: ILookupTree[]): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const mergedEntries = new WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>();
|
||||
for (const tree of trees) {
|
||||
mergeSubtree(result, tree, mergedEntries);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges the data for the mergeTree into the mainTree.
|
||||
*
|
||||
* @param mainTree The tree where the values should be merged
|
||||
* @param mergeTree The tree to be merged into the mainTree
|
||||
* @param mergedEntries WeakMap to track already merged entry pairs
|
||||
*/
|
||||
function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
|
||||
// Need to fix this recursively (and handle lookups)
|
||||
for (const [glyphId, value] of Object.entries(mergeTree.individual)) {
|
||||
// The main tree is guaranteed to have no overlaps between the
|
||||
// individual and range values, so if we match an invididual, there
|
||||
// must not be a range
|
||||
if (mainTree.individual[glyphId]) {
|
||||
mergeTreeEntry(mainTree.individual[glyphId], value, mergedEntries);
|
||||
} else {
|
||||
let matched = false;
|
||||
for (const [index, { range, entry }] of mainTree.range.entries()) {
|
||||
const overlap = getIndividualOverlap(Number(glyphId), range);
|
||||
|
||||
// Don't overlap
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matched = true;
|
||||
|
||||
// If they overlap, we have to split the range and then
|
||||
// merge the overlap
|
||||
mainTree.individual[glyphId] = value;
|
||||
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
|
||||
|
||||
// When there's an overlap, we also have to fix up the range
|
||||
// that we had already processed
|
||||
mainTree.range.splice(index, 1);
|
||||
for (const glyph of overlap.second) {
|
||||
if (Array.isArray(glyph)) {
|
||||
mainTree.range.push({
|
||||
range: glyph,
|
||||
entry: cloneEntry(entry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[glyph] = cloneEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
mainTree.individual[glyphId] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { range, entry } of mergeTree.range) {
|
||||
// Ranges are more complicated, because they can overlap with
|
||||
// multiple things, individual and range alike. We start by
|
||||
// eliminating ranges that are already present in another range
|
||||
let remainingRanges: (number | [number, number])[] = [range];
|
||||
|
||||
for (let index = 0; index < mainTree.range.length; index++) {
|
||||
const { range, entry: resultEntry } = mainTree.range[index];
|
||||
for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
|
||||
if (Array.isArray(remainingRange)) {
|
||||
const overlap = getRangeOverlap(remainingRange, range);
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mainTree.range.splice(index, 1);
|
||||
index--;
|
||||
|
||||
const entryToMerge: ILookupTreeEntry = cloneEntry(resultEntry);
|
||||
if (Array.isArray(overlap.both)) {
|
||||
mainTree.range.push({
|
||||
range: overlap.both,
|
||||
entry: entryToMerge
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[overlap.both] = entryToMerge;
|
||||
}
|
||||
|
||||
mergeTreeEntry(entryToMerge, cloneEntry(entry), mergedEntries);
|
||||
|
||||
for (const second of overlap.second) {
|
||||
if (Array.isArray(second)) {
|
||||
mainTree.range.push({
|
||||
range: second,
|
||||
entry: cloneEntry(resultEntry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[second] = cloneEntry(resultEntry);
|
||||
}
|
||||
}
|
||||
|
||||
remainingRanges = overlap.first;
|
||||
} else {
|
||||
const overlap = getIndividualOverlap(remainingRange, range);
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If they overlap, we have to split the range and then
|
||||
// merge the overlap
|
||||
mainTree.individual[remainingRange] = cloneEntry(entry);
|
||||
mergeTreeEntry(mainTree.individual[remainingRange], cloneEntry(resultEntry), mergedEntries);
|
||||
|
||||
// When there's an overlap, we also have to fix up the range
|
||||
// that we had already processed
|
||||
mainTree.range.splice(index, 1);
|
||||
index--;
|
||||
|
||||
for (const glyph of overlap.second) {
|
||||
if (Array.isArray(glyph)) {
|
||||
mainTree.range.push({
|
||||
range: glyph,
|
||||
entry: cloneEntry(resultEntry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[glyph] = cloneEntry(resultEntry);
|
||||
}
|
||||
}
|
||||
|
||||
remainingRanges.splice(remainingIndex, 1, ...overlap.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, we run the same against any individual glyphs
|
||||
for (const glyphId of Object.keys(mainTree.individual)) {
|
||||
for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
|
||||
if (Array.isArray(remainingRange)) {
|
||||
const overlap = getIndividualOverlap(Number(glyphId), remainingRange);
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If they overlap, we have to merge the overlap
|
||||
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
|
||||
|
||||
// Update the remaining ranges
|
||||
remainingRanges.splice(remainingIndex, 1, ...overlap.second);
|
||||
break;
|
||||
} else {
|
||||
if (Number(glyphId) === remainingRange) {
|
||||
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining ranges should just be added directly
|
||||
for (const remainingRange of remainingRanges) {
|
||||
if (Array.isArray(remainingRange)) {
|
||||
mainTree.range.push({
|
||||
range: remainingRange,
|
||||
entry: cloneEntry(entry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[remainingRange] = cloneEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges the entry forr the mergeTree into the mainTree
|
||||
*
|
||||
* @param mainTree The entry where the values should be merged
|
||||
* @param mergeTree The entry to merge into the mainTree
|
||||
* @param mergedEntries WeakMap to track already merged entry pairs
|
||||
*/
|
||||
function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
|
||||
// Check if we've already merged this pair
|
||||
let mergedSet = mergedEntries.get(mainTree);
|
||||
if (mergedSet?.has(mergeTree)) {
|
||||
return;
|
||||
}
|
||||
if (!mergedSet) {
|
||||
mergedSet = new Set();
|
||||
mergedEntries.set(mainTree, mergedSet);
|
||||
}
|
||||
mergedSet.add(mergeTree);
|
||||
|
||||
if (
|
||||
mergeTree.lookup && (
|
||||
!mainTree.lookup ||
|
||||
mainTree.lookup.index > mergeTree.lookup.index ||
|
||||
(mainTree.lookup.index === mergeTree.lookup.index && mainTree.lookup.subIndex > mergeTree.lookup.subIndex)
|
||||
)
|
||||
) {
|
||||
mainTree.lookup = mergeTree.lookup;
|
||||
}
|
||||
|
||||
if (mergeTree.forward) {
|
||||
if (!mainTree.forward) {
|
||||
mainTree.forward = mergeTree.forward;
|
||||
} else {
|
||||
mergeSubtree(mainTree.forward, mergeTree.forward, mergedEntries);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergeTree.reverse) {
|
||||
if (!mainTree.reverse) {
|
||||
mainTree.reverse = mergeTree.reverse;
|
||||
} else {
|
||||
mergeSubtree(mainTree.reverse, mergeTree.reverse, mergedEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IOverlap {
|
||||
first: (number | [number, number])[];
|
||||
second: (number | [number, number])[];
|
||||
both: number | [number, number] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the overlap (if any) between two ranges. Returns the distinct
|
||||
* ranges for each range and the overlap (if any).
|
||||
*
|
||||
* @param first First range
|
||||
* @param second Second range
|
||||
*/
|
||||
function getRangeOverlap(first: [number, number], second: [number, number]): IOverlap {
|
||||
const result: IOverlap = {
|
||||
first: [],
|
||||
second: [],
|
||||
both: null
|
||||
};
|
||||
|
||||
// Both
|
||||
if (first[0] < second[1] && second[0] < first[1]) {
|
||||
const start = Math.max(first[0], second[0]);
|
||||
const end = Math.min(first[1], second[1]);
|
||||
result.both = rangeOrIndividual(start, end);
|
||||
}
|
||||
|
||||
// Before
|
||||
if (first[0] < second[0]) {
|
||||
const start = first[0];
|
||||
const end = Math.min(second[0], first[1]);
|
||||
result.first.push(rangeOrIndividual(start, end));
|
||||
} else if (second[0] < first[0]) {
|
||||
const start = second[0];
|
||||
const end = Math.min(second[1], first[0]);
|
||||
result.second.push(rangeOrIndividual(start, end));
|
||||
}
|
||||
|
||||
// After
|
||||
if (first[1] > second[1]) {
|
||||
const start = Math.max(first[0], second[1]);
|
||||
const end = first[1];
|
||||
result.first.push(rangeOrIndividual(start, end));
|
||||
} else if (second[1] > first[1]) {
|
||||
const start = Math.max(first[1], second[0]);
|
||||
const end = second[1];
|
||||
result.second.push(rangeOrIndividual(start, end));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the overlap (if any) between the individual glyph and the range
|
||||
* provided. Returns the glyphs and/or ranges that are unique to each provided
|
||||
* and the overlap (if any).
|
||||
*
|
||||
* @param first Individual glyph
|
||||
* @param second Range
|
||||
*/
|
||||
function getIndividualOverlap(first: number, second: [number, number]): IOverlap {
|
||||
// Disjoint
|
||||
if (first < second[0] || first > second[1]) {
|
||||
return {
|
||||
first: [first],
|
||||
second: [second],
|
||||
both: null
|
||||
};
|
||||
}
|
||||
|
||||
const result: IOverlap = {
|
||||
first: [],
|
||||
second: [],
|
||||
both: first
|
||||
};
|
||||
|
||||
if (second[0] < first) {
|
||||
result.second.push(rangeOrIndividual(second[0], first));
|
||||
}
|
||||
|
||||
if (second[1] > first) {
|
||||
result.second.push(rangeOrIndividual(first + 1, second[1]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an individual glyph if the range is of size one or a range if it is
|
||||
* larger.
|
||||
*
|
||||
* @param start Beginning of the range (inclusive)
|
||||
* @param end End of the range (exclusive)
|
||||
*/
|
||||
function rangeOrIndividual(start: number, end: number): number | [number, number] {
|
||||
if (end - start === 1) {
|
||||
return start;
|
||||
}
|
||||
return [start, end];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an individual lookup tree entry.
|
||||
*
|
||||
* @param entry Lookup tree entry to clone
|
||||
* @param visited Map to track already cloned entries (prevents infinite loops)
|
||||
*/
|
||||
function cloneEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTreeEntry {
|
||||
if (visited.has(entry)) {
|
||||
return visited.get(entry)!;
|
||||
}
|
||||
|
||||
const result: ILookupTreeEntry = {};
|
||||
visited.set(entry, result);
|
||||
|
||||
if (entry.forward) {
|
||||
result.forward = cloneTree(entry.forward, visited);
|
||||
}
|
||||
|
||||
if (entry.reverse) {
|
||||
result.reverse = cloneTree(entry.reverse, visited);
|
||||
}
|
||||
|
||||
if (entry.lookup) {
|
||||
result.lookup = {
|
||||
contextRange: entry.lookup.contextRange.slice() as [number, number],
|
||||
index: entry.lookup.index,
|
||||
length: entry.lookup.length,
|
||||
subIndex: entry.lookup.subIndex,
|
||||
substitutions: entry.lookup.substitutions.slice()
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones a lookup tree.
|
||||
*
|
||||
* @param tree Lookup tree to clone
|
||||
* @param visited Map to track already cloned entries (prevents infinite loops)
|
||||
*/
|
||||
function cloneTree(tree: ILookupTree, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTree {
|
||||
const individual: { [glyphId: string]: ILookupTreeEntry } = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
individual[glyphId] = cloneEntry(entry, visited);
|
||||
}
|
||||
|
||||
return {
|
||||
individual,
|
||||
range: tree.range.map(({ range, entry }) => ({
|
||||
range: range.slice() as [number, number],
|
||||
entry: cloneEntry(entry, visited)
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import mergeRange from './mergeRange';
|
||||
|
||||
describe('addon-ligatures - mergeRange', () => {
|
||||
it('inserts a new range before the existing ones', () => {
|
||||
const result = mergeRange([[1, 2], [2, 3]], 0, 1);
|
||||
assert.deepEqual(result, [[0, 1], [1, 2], [2, 3]]);
|
||||
});
|
||||
|
||||
it('inserts in between two ranges', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 2, 4);
|
||||
assert.deepEqual(result, [[0, 2], [2, 4], [4, 6]]);
|
||||
});
|
||||
|
||||
it('inserts after the last range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 6, 8);
|
||||
assert.deepEqual(result, [[0, 2], [4, 6], [6, 8]]);
|
||||
});
|
||||
|
||||
it('extends the beginning of a range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 3, 5);
|
||||
assert.deepEqual(result, [[0, 2], [3, 6]]);
|
||||
});
|
||||
|
||||
it('extends the end of a range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 1, 4);
|
||||
assert.deepEqual(result, [[0, 4], [4, 6]]);
|
||||
});
|
||||
|
||||
it('extends the last range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 5, 7);
|
||||
assert.deepEqual(result, [[0, 2], [4, 7]]);
|
||||
});
|
||||
|
||||
it('connects two ranges', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 1, 5);
|
||||
assert.deepEqual(result, [[0, 6]]);
|
||||
});
|
||||
|
||||
it('connects more than two ranges', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6], [8, 10], [12, 14]], 1, 10);
|
||||
assert.deepEqual(result, [[0, 10], [12, 14]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Merges the range defined by the provided start and end into the list of
|
||||
* existing ranges. The merge is done in place on the existing range for
|
||||
* performance and is also returned.
|
||||
*
|
||||
* @param ranges Existing range list
|
||||
* @param newRangeStart Start position of the range to merge, inclusive
|
||||
* @param newRangeEnd End position of range to merge, exclusive
|
||||
*/
|
||||
export default function mergeRange(ranges: [number, number][], newRangeStart: number, newRangeEnd: number): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, [newRangeStart, newRangeEnd]);
|
||||
return ranges;
|
||||
}
|
||||
if (newRangeEnd <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
return ranges;
|
||||
}
|
||||
if (newRangeStart < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
inRange = true;
|
||||
} else {
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRangeEnd;
|
||||
return ranges;
|
||||
}
|
||||
if (newRangeEnd <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
}
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRangeEnd;
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push([newRangeStart, newRangeEnd]);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
|
||||
import { ILookupTree } from '../types';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 6, format 1.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chaining-context-substitution-format-1-simple-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param lookups List of lookup tables
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat1, lookups: Lookup[], tableIndex: number): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId, index } of firstGlyphs) {
|
||||
const chainRuleSet = table.chainRuleSets[index];
|
||||
|
||||
// If the chain rule set is null there's nothing to do with this table.
|
||||
if (!chainRuleSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [subIndex, subTable] of chainRuleSet.entries()) {
|
||||
let currentEntries: IEntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const [index, glyph] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
[glyph],
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
length: subTable.input.length + 1,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
|
||||
import { ILookupTree } from '../types';
|
||||
import mergeTrees from '../merge';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import getGlyphClass, { listClassGlyphs } from './classDef';
|
||||
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 6, format 2.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chaining-context-substitution-format-2-class-based-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param lookups List of lookup tables
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat2, lookups: Lookup[], tableIndex: number): ILookupTree {
|
||||
const results: ILookupTree[] = [];
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
const firstInputClass = getGlyphClass(table.inputClassDef, glyphId);
|
||||
for (const [glyphId, inputClass] of firstInputClass.entries()) {
|
||||
// istanbul ignore next - invalid font
|
||||
if (inputClass === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const classSet = table.chainClassSet[inputClass];
|
||||
|
||||
// If the class set is null there's nothing to do with this table.
|
||||
if (!classSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [subIndex, subTable] of classSet.entries()) {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
let currentEntries: IEntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
for (const [index, classNum] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listClassGlyphs(table.inputClassDef, classNum),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listClassGlyphs(table.lookaheadClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listClassGlyphs(table.backtrackClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
length: subTable.input.length + 1,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeTrees(results);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
|
||||
import { ILookupTree } from '../types';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 6, format 3.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#63-chaining-context-substitution-format-3-coverage-based-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param lookups List of lookup tables
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat3, lookups: Lookup[], tableIndex: number): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]);
|
||||
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
let currentEntries: IEntryMeta[] = getInputTree(
|
||||
result,
|
||||
table.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
for (const [index, coverage] of table.inputCoverage.slice(1).entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
table.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: table.inputCoverage.length,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
table.inputCoverage.length + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { IReverseChainingContextualSingleSubstitutionTable } from '../tables';
|
||||
import { ILookupTree, ILookupTreeEntry } from '../types';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import { processLookaheadPosition, processBacktrackPosition, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 8, format 1.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#81-reverse-chaining-contextual-single-substitution-format-1-coverage-based-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: IReverseChainingContextualSingleSubstitutionTable, tableIndex: number): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const glyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId, index } of glyphs) {
|
||||
const initialEntry: ILookupTreeEntry = {};
|
||||
if (Array.isArray(glyphId)) {
|
||||
result.range.push({
|
||||
entry: initialEntry,
|
||||
range: glyphId
|
||||
});
|
||||
} else {
|
||||
result.individual[glyphId] = initialEntry;
|
||||
}
|
||||
|
||||
let currentEntries: IEntryMeta[] = [{
|
||||
entry: initialEntry,
|
||||
substitutions: [table.substitutes[index]]
|
||||
}];
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: 1,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
1 + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ClassDefTable } from '../tables';
|
||||
|
||||
/**
|
||||
* Get the number of the class to which the glyph belongs, or null if it doesn't
|
||||
* belong to any of them.
|
||||
*
|
||||
* @param table JSON representation of the class def table
|
||||
* @param glyphId Index of the glyph to look for
|
||||
*/
|
||||
export default function getGlyphClass(table: ClassDefTable, glyphId: number | [number, number]): Map<number | [number, number], number | null> {
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2
|
||||
case 2:
|
||||
if (Array.isArray(glyphId)) {
|
||||
return getRangeGlyphClass(table, glyphId);
|
||||
}
|
||||
return new Map([[
|
||||
glyphId,
|
||||
getIndividualGlyphClass(table, glyphId)
|
||||
]]);
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-1
|
||||
default:
|
||||
return new Map([[glyphId, null]]);
|
||||
}
|
||||
}
|
||||
|
||||
function getRangeGlyphClass(table: ClassDefTable.IFormat2, glyphId: [number, number]): Map<number | [number, number], number | null> {
|
||||
const classStart: number = glyphId[0];
|
||||
const currentClass: number | null = getIndividualGlyphClass(table, classStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
|
||||
while (search < glyphId[1]) {
|
||||
const clazz = getIndividualGlyphClass(table, search);
|
||||
if (clazz !== currentClass) {
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
}
|
||||
search++;
|
||||
}
|
||||
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getIndividualGlyphClass(table: ClassDefTable.IFormat2, glyphId: number): number | null {
|
||||
for (const range of table.ranges) {
|
||||
if (range.start <= glyphId && range.end >= glyphId) {
|
||||
return range.classId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function listClassGlyphs(table: ClassDefTable, index: number): (number | [number, number])[] {
|
||||
switch (table.format) {
|
||||
case 2:
|
||||
const results: (number | [number, number])[] = [];
|
||||
for (const range of table.ranges) {
|
||||
if (range.classId !== index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (range.end === range.start) {
|
||||
results.push(range.start);
|
||||
} else {
|
||||
results.push([range.start, range.end + 1]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { CoverageTable } from '../tables';
|
||||
|
||||
/**
|
||||
* Get the index of the given glyph in the coverage table, or null if it is not
|
||||
* present in the table.
|
||||
*
|
||||
* @param table JSON representation of the coverage table
|
||||
* @param glyphId Index of the glyph to look for
|
||||
*/
|
||||
export default function getCoverageGlyphIndex(table: CoverageTable, glyphId: number): number | null {
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1
|
||||
case 1:
|
||||
const index = table.glyphs.indexOf(glyphId);
|
||||
return index !== -1
|
||||
? index
|
||||
: null;
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2
|
||||
case 2:
|
||||
const range = table.ranges
|
||||
.find(range => range.start <= glyphId && range.end >= glyphId);
|
||||
return range
|
||||
? range.index
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number], index: number }[] {
|
||||
switch (table.format) {
|
||||
case 1:
|
||||
return table.glyphs.map((glyphId, index) => ({ glyphId, index }));
|
||||
case 2:
|
||||
const results: { glyphId: number | [number, number], index: number }[] = [];
|
||||
for (const [index, range] of table.ranges.entries()) {
|
||||
if (range.end === range.start) {
|
||||
results.push({ glyphId: range.start, index });
|
||||
} else {
|
||||
results.push({ glyphId: [range.start, range.end + 1], index });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { ILookupTreeEntry, ILookupTree } from '../types';
|
||||
import { ISubstitutionLookupRecord, Lookup } from '../tables';
|
||||
|
||||
import { getIndividualSubstitutionGlyph, getRangeSubstitutionGlyphs } from './substitution';
|
||||
|
||||
export interface IEntryMeta {
|
||||
entry: ILookupTreeEntry;
|
||||
substitutions: (number | null)[];
|
||||
}
|
||||
|
||||
export function processInputPosition(
|
||||
glyphs: (number | [number, number])[],
|
||||
position: number,
|
||||
currentEntries: IEntryMeta[],
|
||||
lookupRecords: ISubstitutionLookupRecord[],
|
||||
lookups: Lookup[]
|
||||
): IEntryMeta[] {
|
||||
const nextEntries: IEntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
currentEntry.entry.forward = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
for (const glyph of glyphs) {
|
||||
nextEntries.push(...getInputTree(
|
||||
currentEntry.entry.forward,
|
||||
lookupRecords,
|
||||
lookups,
|
||||
position,
|
||||
glyph
|
||||
).map(({ entry, substitution }) => ({
|
||||
entry,
|
||||
substitutions: [...currentEntry.substitutions, substitution]
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
export function processLookaheadPosition(
|
||||
glyphs: (number | [number, number])[],
|
||||
currentEntries: IEntryMeta[]
|
||||
): IEntryMeta[] {
|
||||
const nextEntries: IEntryMeta[] = [];
|
||||
const processedEntries = new Set<ILookupTreeEntry>();
|
||||
|
||||
for (const currentEntry of currentEntries) {
|
||||
// Skip if we've already processed this entry object
|
||||
if (processedEntries.has(currentEntry.entry)) {
|
||||
continue;
|
||||
}
|
||||
processedEntries.add(currentEntry.entry);
|
||||
|
||||
currentEntry.entry.forward ??= {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
// All glyphs at this position share ONE entry - lookahead just needs to match,
|
||||
// all paths lead to the same result
|
||||
const sharedEntry: ILookupTreeEntry = {};
|
||||
|
||||
for (const glyph of glyphs) {
|
||||
if (Array.isArray(glyph)) {
|
||||
currentEntry.entry.forward.range.push({
|
||||
entry: sharedEntry,
|
||||
range: glyph
|
||||
});
|
||||
} else {
|
||||
currentEntry.entry.forward.individual[glyph] = sharedEntry;
|
||||
}
|
||||
}
|
||||
|
||||
nextEntries.push({
|
||||
entry: sharedEntry,
|
||||
substitutions: currentEntry.substitutions
|
||||
});
|
||||
}
|
||||
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
export function processBacktrackPosition(
|
||||
glyphs: (number | [number, number])[],
|
||||
currentEntries: IEntryMeta[]
|
||||
): IEntryMeta[] {
|
||||
const nextEntries: IEntryMeta[] = [];
|
||||
const processedEntries = new Set<ILookupTreeEntry>();
|
||||
|
||||
for (const currentEntry of currentEntries) {
|
||||
// Skip if we've already processed this entry object
|
||||
if (processedEntries.has(currentEntry.entry)) {
|
||||
continue;
|
||||
}
|
||||
processedEntries.add(currentEntry.entry);
|
||||
|
||||
currentEntry.entry.reverse ??= {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
// All glyphs at this position share ONE entry - backtrack just needs to match,
|
||||
// all paths lead to the same result
|
||||
const sharedEntry: ILookupTreeEntry = {};
|
||||
|
||||
for (const glyph of glyphs) {
|
||||
if (Array.isArray(glyph)) {
|
||||
currentEntry.entry.reverse.range.push({
|
||||
entry: sharedEntry,
|
||||
range: glyph
|
||||
});
|
||||
} else {
|
||||
currentEntry.entry.reverse.individual[glyph] = sharedEntry;
|
||||
}
|
||||
}
|
||||
|
||||
nextEntries.push({
|
||||
entry: sharedEntry,
|
||||
substitutions: currentEntry.substitutions
|
||||
});
|
||||
}
|
||||
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
export function getInputTree(tree: ILookupTree, substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: ILookupTreeEntry, substitution: number | null }[] {
|
||||
const result: { entry: ILookupTreeEntry, substitution: number | null }[] = [];
|
||||
if (!Array.isArray(glyphId)) {
|
||||
tree.individual[glyphId] = {};
|
||||
result.push({
|
||||
entry: tree.individual[glyphId],
|
||||
substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId)
|
||||
});
|
||||
} else {
|
||||
const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId);
|
||||
for (const [range, substitution] of subs) {
|
||||
const entry: ILookupTreeEntry = {};
|
||||
if (Array.isArray(range)) {
|
||||
tree.range.push({ range, entry });
|
||||
} else {
|
||||
tree.individual[range] = {};
|
||||
}
|
||||
result.push({ entry, substitution });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSubstitutionAtPositionRange(substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map<number | [number, number], number | null> {
|
||||
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
|
||||
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.IType1).subtables) {
|
||||
const sub = getRangeSubstitutionGlyphs(
|
||||
substitutionTable,
|
||||
range
|
||||
);
|
||||
|
||||
if (!Array.from(sub.values()).every(val => val !== null)) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Map([[range, null]]);
|
||||
}
|
||||
|
||||
function getSubstitutionAtPosition(substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null {
|
||||
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
|
||||
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.IType1).subtables) {
|
||||
const sub = getIndividualSubstitutionGlyph(
|
||||
substitutionTable,
|
||||
glyphId
|
||||
);
|
||||
|
||||
if (sub !== null) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { SubstitutionTable } from '../tables';
|
||||
|
||||
import getCoverageGlyphIndex from './coverage';
|
||||
|
||||
/**
|
||||
* Get the substitution glyph for the givne glyph, or null if the glyph was not
|
||||
* found in the table.
|
||||
*
|
||||
* @param table JSON representation of the substitution table
|
||||
* @param glyphId The index of the glpyh to find substitutions for
|
||||
*/
|
||||
export function getRangeSubstitutionGlyphs(table: SubstitutionTable, glyphId: [number, number]): Map<[number, number] | number, number | null> {
|
||||
const replacementStart: number = glyphId[0];
|
||||
const currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
|
||||
while (search < glyphId[1]) {
|
||||
const sub = getIndividualSubstitutionGlyph(table, search);
|
||||
if (sub !== currentReplacement) {
|
||||
if (search - replacementStart <= 1) {
|
||||
result.set(replacementStart, currentReplacement);
|
||||
} else {
|
||||
result.set([replacementStart, search], currentReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
search++;
|
||||
}
|
||||
|
||||
if (search - replacementStart <= 1) {
|
||||
result.set(replacementStart, currentReplacement);
|
||||
} else {
|
||||
result.set([replacementStart, search], currentReplacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId: number): number | null {
|
||||
const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId);
|
||||
|
||||
// istanbul ignore next - invalid font
|
||||
if (coverageIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (table.substFormat) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1
|
||||
case 1:
|
||||
// TODO: determine if there's a rhyme or reason to the 16-bit
|
||||
// wraparound and if it can ever be a different number
|
||||
return (glyphId + table.deltaGlyphId) % (2 ** 16);
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
|
||||
case 2:
|
||||
return table.substitute[coverageIndex] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
export type SubstitutionTable = SubstitutionTable.IFormat1 | SubstitutionTable.IFormat2;
|
||||
export namespace SubstitutionTable {
|
||||
export interface IFormat1 {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
deltaGlyphId: number;
|
||||
}
|
||||
|
||||
export interface IFormat2 {
|
||||
substFormat: 2;
|
||||
coverage: CoverageTable;
|
||||
substitute: number[];
|
||||
}
|
||||
}
|
||||
|
||||
export type CoverageTable = CoverageTable.IFormat1 | CoverageTable.IFormat2;
|
||||
export namespace CoverageTable {
|
||||
export interface IFormat1 {
|
||||
format: 1;
|
||||
glyphs: number[];
|
||||
}
|
||||
|
||||
export interface IFormat2 {
|
||||
format: 2;
|
||||
ranges: {
|
||||
start: number;
|
||||
end: number;
|
||||
index: number;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.IFormat1 |
|
||||
ChainingContextualSubstitutionTable.IFormat2 | ChainingContextualSubstitutionTable.IFormat3;
|
||||
export namespace ChainingContextualSubstitutionTable {
|
||||
export interface IFormat1 {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
chainRuleSets: ChainSubRuleTable[][];
|
||||
}
|
||||
|
||||
export interface IFormat2 {
|
||||
substFormat: 2;
|
||||
coverage: CoverageTable;
|
||||
backtrackClassDef: ClassDefTable;
|
||||
inputClassDef: ClassDefTable;
|
||||
lookaheadClassDef: ClassDefTable;
|
||||
chainClassSet: (null | IChainSubClassRuleTable[])[];
|
||||
}
|
||||
|
||||
export interface IFormat3 {
|
||||
substFormat: 3;
|
||||
backtrackCoverage: CoverageTable[];
|
||||
inputCoverage: CoverageTable[];
|
||||
lookaheadCoverage: CoverageTable[];
|
||||
lookupRecords: ISubstitutionLookupRecord[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface IReverseChainingContextualSingleSubstitutionTable {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
backtrackCoverage: CoverageTable[];
|
||||
lookaheadCoverage: CoverageTable[];
|
||||
substitutes: number[];
|
||||
}
|
||||
|
||||
export type ClassDefTable = ClassDefTable.IFormat2;
|
||||
export namespace ClassDefTable {
|
||||
export interface IFormat2 {
|
||||
format: 2;
|
||||
ranges: {
|
||||
start: number;
|
||||
end: number;
|
||||
classId: number;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISubstitutionLookupRecord {
|
||||
sequenceIndex: number;
|
||||
lookupListIndex: number;
|
||||
}
|
||||
|
||||
export type ChainSubRuleTable = IChainSubClassRuleTable;
|
||||
export interface IChainSubClassRuleTable {
|
||||
backtrack: number[];
|
||||
input: number[];
|
||||
lookahead: number[];
|
||||
lookupRecords: ISubstitutionLookupRecord[];
|
||||
}
|
||||
|
||||
export type Lookup = Lookup.IType1 | Lookup.IType6 | Lookup.IType8;
|
||||
export namespace Lookup {
|
||||
export interface IType1 {
|
||||
lookupType: 1;
|
||||
lookupFlag: number;
|
||||
subtables: SubstitutionTable[];
|
||||
}
|
||||
|
||||
export interface IType6 {
|
||||
lookupType: 6;
|
||||
lookupFlag: number;
|
||||
subtables: ChainingContextualSubstitutionTable[];
|
||||
}
|
||||
|
||||
export interface IType8 {
|
||||
lookupType: 8;
|
||||
lookupFlag: number;
|
||||
subtables: IReverseChainingContextualSingleSubstitutionTable[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
export interface ISubstitutionResult {
|
||||
index: number;
|
||||
contextRange: [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about ligatures found in a sequence of text
|
||||
*/
|
||||
export interface ILigatureData {
|
||||
/**
|
||||
* The list of font glyphs in the input text.
|
||||
*/
|
||||
inputGlyphs: number[];
|
||||
|
||||
/**
|
||||
* The list of font glyphs after performing replacements for font ligatures.
|
||||
*/
|
||||
outputGlyphs: number[];
|
||||
|
||||
/**
|
||||
* Sorted array of ranges that must be rendered together to produce the
|
||||
* ligatures in the output sequence. The ranges are inclusive on the left and
|
||||
* exclusive on the right.
|
||||
*/
|
||||
contextRanges: [number, number][];
|
||||
}
|
||||
|
||||
export interface IFont {
|
||||
/**
|
||||
* Scans the provided text for font ligatures, returning an object with
|
||||
* metadata about the text and any ligatures found.
|
||||
*
|
||||
* @param text String to search for ligatures
|
||||
*/
|
||||
findLigatures(text: string): ILigatureData;
|
||||
|
||||
/**
|
||||
* Scans the provided text for font ligatures, returning an array of ranges
|
||||
* where ligatures are located.
|
||||
*
|
||||
* @param text String to search for ligatures
|
||||
*/
|
||||
findLigatureRanges(text: string): [number, number][];
|
||||
}
|
||||
|
||||
export interface IOptions {
|
||||
/**
|
||||
* Optional size of previous results to store, measured in total number of
|
||||
* characters from input strings. Defaults to no cache (0)
|
||||
*/
|
||||
cacheSize?: number;
|
||||
}
|
||||
|
||||
export interface ILookupTree {
|
||||
individual: {
|
||||
[glyphId: string]: ILookupTreeEntry;
|
||||
};
|
||||
range: {
|
||||
range: [number, number];
|
||||
entry: ILookupTreeEntry;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ILookupTreeEntry {
|
||||
lookup?: ILookupResult;
|
||||
forward?: ILookupTree;
|
||||
reverse?: ILookupTree;
|
||||
}
|
||||
|
||||
export interface ILookupResult {
|
||||
substitutions: (number | null)[];
|
||||
length: number;
|
||||
index: number;
|
||||
subIndex: number;
|
||||
contextRange: [number, number];
|
||||
}
|
||||
|
||||
export interface IFlattenedLookupTree {
|
||||
[glyphId: string]: IFlattenedLookupTreeEntry;
|
||||
}
|
||||
|
||||
export interface IFlattenedLookupTreeEntry {
|
||||
lookup?: ILookupResult;
|
||||
forward?: IFlattenedLookupTree;
|
||||
reverse?: IFlattenedLookupTree;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { IFlattenedLookupTree, ILookupResult } from './types';
|
||||
|
||||
export default function walkTree(tree: IFlattenedLookupTree, sequence: number[], startIndex: number, index: number): ILookupResult | undefined {
|
||||
const glyphId = sequence[index];
|
||||
const subtree = tree[glyphId];
|
||||
if (!subtree) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let lookup = subtree.lookup;
|
||||
if (subtree.reverse) {
|
||||
const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex);
|
||||
|
||||
if (
|
||||
(!lookup && reverseLookup) ||
|
||||
(
|
||||
reverseLookup && lookup && (
|
||||
lookup.index > reverseLookup.index ||
|
||||
(lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex)
|
||||
)
|
||||
)
|
||||
) {
|
||||
lookup = reverseLookup;
|
||||
}
|
||||
}
|
||||
|
||||
if (++index >= sequence.length || !subtree.forward) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index);
|
||||
|
||||
if (
|
||||
(!lookup && forwardLookup) ||
|
||||
(
|
||||
forwardLookup && lookup && (
|
||||
lookup.index > forwardLookup.index ||
|
||||
(lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex)
|
||||
)
|
||||
)
|
||||
) {
|
||||
lookup = forwardLookup;
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function walkReverse(tree: IFlattenedLookupTree, sequence: number[], index: number): ILookupResult | undefined {
|
||||
let subtree = tree[sequence[--index]];
|
||||
let lookup: ILookupResult | undefined = subtree && subtree.lookup;
|
||||
while (subtree) {
|
||||
if (
|
||||
(!lookup && subtree.lookup) ||
|
||||
(subtree.lookup && lookup && lookup.index > subtree.lookup.index)
|
||||
) {
|
||||
lookup = subtree.lookup;
|
||||
}
|
||||
|
||||
if (--index < 0 || !subtree.reverse) {
|
||||
break;
|
||||
}
|
||||
|
||||
subtree = subtree.reverse[sequence[index]];
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
import { Font } from 'font-ligatures';
|
||||
import { Font } from './fontLigatures/index';
|
||||
|
||||
import load from './font';
|
||||
|
||||
@@ -30,7 +30,7 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
|
||||
let currentFontName: string | undefined = undefined;
|
||||
let font: Font | undefined = undefined;
|
||||
let loadingState: LoadingState = LoadingState.UNLOADED;
|
||||
let loadError: any | undefined = undefined;
|
||||
let loadError: unknown = undefined;
|
||||
|
||||
return term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||
// If the font hasn't been loaded yet, load it and return an empty result
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
// TODO: integrate tests from http://test.csswg.org/suites/css-fonts-4_dev/nightly-unstable/
|
||||
describe('parse', () => {
|
||||
describe('addon-ligatures - parse', () => {
|
||||
it('parses individual families', () => {
|
||||
assert.deepEqual(parse('monospace'), ['monospace']);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"noUnusedLocals": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
"../../../node_modules/@types/mocha",
|
||||
// HACK: src shouldn't use node types but it's needed for index.test.ts
|
||||
"../../../node_modules/@types/node"
|
||||
],
|
||||
"paths": {
|
||||
"@xterm/addon-ligatures" : [
|
||||
|
||||
+20
-19
@@ -4,61 +4,62 @@
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
import { assert } from 'chai';
|
||||
import * as fontFinder from 'font-finder';
|
||||
|
||||
import * as ligatureSupport from '.';
|
||||
// Use require to get a mutable module object (ESM imports create read-only bindings)
|
||||
const fontFinder = require('font-finder');
|
||||
const ligatureSupport = require('../out-esbuild/index');
|
||||
|
||||
const originalList = fontFinder.list;
|
||||
|
||||
describe('LigaturesAddon', () => {
|
||||
let onRefresh: sinon.SinonStub;
|
||||
let onRefresh: { called: boolean, callCount: number, (...args: any[]): void };
|
||||
let term: MockTerminal;
|
||||
|
||||
// -> forms a ligature in Fira Code and Iosevka, but www only forms a ligature
|
||||
// in Fira Code
|
||||
const input = 'a -> b www c';
|
||||
|
||||
before(() => {
|
||||
sinon.stub(fontFinder, 'list').returns(Promise.resolve({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
fontFinder.list = () => Promise.resolve({
|
||||
'Fira Code': [{
|
||||
path: path.join(__dirname, '../fonts/firaCode.otf'),
|
||||
style: fontFinder.Style.Regular,
|
||||
type: fontFinder.Type.Monospace,
|
||||
weight: 400
|
||||
}],
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Iosevka': [{
|
||||
path: path.join(__dirname, '../fonts/iosevka.ttf'),
|
||||
style: fontFinder.Style.Regular,
|
||||
type: fontFinder.Type.Monospace,
|
||||
weight: 400
|
||||
}],
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Nonexistant Font': [{
|
||||
path: path.join(__dirname, '../fonts/nonexistant.ttf'),
|
||||
style: fontFinder.Style.Regular,
|
||||
type: fontFinder.Type.Monospace,
|
||||
weight: 400
|
||||
}]
|
||||
} as fontFinder.FontList));
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fontFinder.list = originalList;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onRefresh = sinon.stub();
|
||||
onRefresh = Object.assign((..._args: any[]) => { onRefresh.called = true; onRefresh.callCount++; }, { called: false, callCount: 0 });
|
||||
term = new MockTerminal(onRefresh);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
});
|
||||
|
||||
it('registers itself correctly', () => {
|
||||
const term = new MockTerminal(sinon.spy());
|
||||
const term = new MockTerminal(() => {});
|
||||
assert.isUndefined(term.joiner);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
assert.isFunction(term.joiner);
|
||||
});
|
||||
|
||||
it('registers itself correctly when called directly', () => {
|
||||
const term = new MockTerminal(sinon.spy());
|
||||
const term = new MockTerminal(() => {});
|
||||
assert.isUndefined(term.joiner);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
assert.isFunction(term.joiner);
|
||||
@@ -72,14 +73,14 @@ describe('LigaturesAddon', () => {
|
||||
term.options.fontFamily = 'Nonexistant Font, monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.strictEqual(onRefresh.callCount, 0);
|
||||
});
|
||||
|
||||
it('returns nothing if the font is not present on the system', async () => {
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.strictEqual(onRefresh.callCount, 0);
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
@@ -87,7 +88,7 @@ describe('LigaturesAddon', () => {
|
||||
term.options.fontFamily = 'monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.strictEqual(onRefresh.callCount, 0);
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
@@ -95,7 +96,7 @@ describe('LigaturesAddon', () => {
|
||||
term.options.fontFamily = '';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.strictEqual(onRefresh.callCount, 0);
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
@@ -103,7 +104,7 @@ describe('LigaturesAddon', () => {
|
||||
term.options.fontFamily = {} as any;
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.strictEqual(onRefresh.callCount, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"es2021"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-esbuild-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" }
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,383 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz"
|
||||
integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==
|
||||
dependencies:
|
||||
type-detect "4.0.8"
|
||||
|
||||
"@sinonjs/formatio@^3.0.0", "@sinonjs/formatio@^3.2.1":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz"
|
||||
integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1"
|
||||
"@sinonjs/samsam" "^3.1.0"
|
||||
|
||||
"@sinonjs/samsam@^2.1.2":
|
||||
version "2.1.3"
|
||||
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz"
|
||||
integrity sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==
|
||||
|
||||
"@sinonjs/samsam@^3.1.0":
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz"
|
||||
integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.3.0"
|
||||
array-from "^2.1.1"
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@sinonjs/text-encoding@^0.7.1":
|
||||
version "0.7.1"
|
||||
resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz"
|
||||
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
|
||||
|
||||
"@types/sinon@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.1.tgz"
|
||||
integrity sha512-yxzBCIjE3lp9lYjfBbIK/LRCoXgCLLbIIBIje7eNCcUIIR2CZZtyX5uto2hVoMSMqLrsRrT6mwwUEd0yFgOwpA==
|
||||
|
||||
array-from@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz"
|
||||
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.6.0:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz"
|
||||
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
|
||||
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
diff@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz"
|
||||
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
fd-slicer@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
|
||||
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
font-finder@^1.0.3, font-finder@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz"
|
||||
integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==
|
||||
dependencies:
|
||||
get-system-fonts "^2.0.0"
|
||||
promise-stream-reader "^1.0.1"
|
||||
|
||||
font-ligatures@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz"
|
||||
integrity sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==
|
||||
dependencies:
|
||||
font-finder "^1.0.3"
|
||||
lru-cache "^6.0.0"
|
||||
opentype.js "^0.8.0"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.2.6:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
get-system-fonts@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.0.tgz"
|
||||
integrity sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz"
|
||||
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
||||
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
|
||||
|
||||
just-extend@^4.0.2:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz"
|
||||
integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
|
||||
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
|
||||
|
||||
lodash@^4.17.15:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lolex@^2.7.5:
|
||||
version "2.7.5"
|
||||
resolved "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz"
|
||||
integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==
|
||||
|
||||
lolex@^5.0.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz"
|
||||
integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.7.0"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@0.5.5:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz"
|
||||
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
nise@^1.4.5:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz"
|
||||
integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==
|
||||
dependencies:
|
||||
"@sinonjs/formatio" "^3.2.1"
|
||||
"@sinonjs/text-encoding" "^0.7.1"
|
||||
just-extend "^4.0.2"
|
||||
lolex "^5.0.1"
|
||||
path-to-regexp "^1.7.0"
|
||||
|
||||
opentype.js@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz"
|
||||
integrity sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==
|
||||
dependencies:
|
||||
tiny-inflate "^1.0.2"
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz"
|
||||
integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
pend@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
promise-stream-reader@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz"
|
||||
integrity sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
sinon@6.3.5:
|
||||
version "6.3.5"
|
||||
resolved "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz"
|
||||
integrity sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.0.2"
|
||||
"@sinonjs/formatio" "^3.0.0"
|
||||
"@sinonjs/samsam" "^2.1.2"
|
||||
diff "^3.5.0"
|
||||
lodash.get "^4.4.2"
|
||||
lolex "^2.7.5"
|
||||
nise "^1.4.5"
|
||||
supports-color "^5.5.0"
|
||||
type-detect "^4.0.8"
|
||||
|
||||
supports-color@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
tiny-inflate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz"
|
||||
integrity sha512-fEndoHfuiSlXtvG0EA1gB4uAdy2dbQ3WfOtQw1/Y8NQ9rxtGbGkENwfGmN0wK7OkxDz1lgc5Ei2LQ0oxKDUcVw==
|
||||
|
||||
type-detect@^4.0.8, type-detect@4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
|
||||
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
|
||||
dependencies:
|
||||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -135,6 +135,7 @@ export class DecorationManager extends Disposable {
|
||||
marker,
|
||||
x: range[1],
|
||||
width: range[2],
|
||||
layer: isActiveResult ? 'top' : 'bottom',
|
||||
backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
|
||||
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
|
||||
color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { disposableTimeout } from 'vs/base/common/async';
|
||||
import { SearchLineCache } from './SearchLineCache';
|
||||
@@ -42,6 +42,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
private _decorationManager: DecorationManager | undefined;
|
||||
private _resultTracker = this._register(new SearchResultTracker());
|
||||
|
||||
private readonly _onAfterSearch = this._register(new Emitter<void>());
|
||||
public readonly onAfterSearch = this._onAfterSearch.event;
|
||||
private readonly _onBeforeSearch = this._register(new Emitter<void>());
|
||||
public readonly onBeforeSearch = this._onBeforeSearch.event;
|
||||
|
||||
public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
|
||||
return this._resultTracker.onDidChangeResults;
|
||||
}
|
||||
@@ -98,6 +103,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
this._onBeforeSearch.fire();
|
||||
|
||||
this._state.lastSearchOptions = searchOptions;
|
||||
|
||||
if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
|
||||
@@ -108,6 +115,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
this._fireResults(searchOptions);
|
||||
this._state.cachedSearchTerm = term;
|
||||
|
||||
this._onAfterSearch.fire();
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -173,6 +182,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
this._onBeforeSearch.fire();
|
||||
|
||||
this._state.lastSearchOptions = searchOptions;
|
||||
|
||||
if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
|
||||
@@ -183,6 +194,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
this._fireResults(searchOptions);
|
||||
this._state.cachedSearchTerm = term;
|
||||
|
||||
this._onAfterSearch.fire();
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -209,7 +222,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
* @param result The result to select.
|
||||
* @returns Whether a result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
|
||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
||||
if (!this._terminal || !this._decorationManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -198,9 +198,7 @@ export class SearchEngine {
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
}
|
||||
result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
|
||||
// Search from startRow - 1 to top
|
||||
if (!result) {
|
||||
|
||||
@@ -38,6 +38,9 @@ export class SearchLineCache extends Disposable {
|
||||
private _linesCache: LineCacheEntry[] | undefined;
|
||||
private _linesCacheTimeout = this._register(new MutableDisposable());
|
||||
private _linesCacheDisposables = this._register(new MutableDisposable());
|
||||
// Track access to avoid recreating a timeout on every init call which occurs once per search
|
||||
// result (findNext/findPrevious -> _highlightAllMatches -> find loop).
|
||||
private _lastAccessTimestamp = 0;
|
||||
|
||||
constructor(private readonly _terminal: Terminal) {
|
||||
super();
|
||||
@@ -57,15 +60,34 @@ export class SearchLineCache extends Disposable {
|
||||
);
|
||||
}
|
||||
|
||||
this._linesCacheTimeout.value = disposableTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
|
||||
this._lastAccessTimestamp = Date.now();
|
||||
if (!this._linesCacheTimeout.value) {
|
||||
this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private _destroyLinesCache(): void {
|
||||
this._linesCache = undefined;
|
||||
this._lastAccessTimestamp = 0;
|
||||
this._linesCacheDisposables.clear();
|
||||
this._linesCacheTimeout.clear();
|
||||
}
|
||||
|
||||
private _scheduleLinesCacheTimeout(delay: number): void {
|
||||
this._linesCacheTimeout.value = disposableTimeout(() => {
|
||||
if (!this._linesCache) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const elapsed = now - this._lastAccessTimestamp;
|
||||
if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {
|
||||
this._destroyLinesCache();
|
||||
return;
|
||||
}
|
||||
this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
public getLineFromCache(row: number): LineCacheEntry | undefined {
|
||||
return this._linesCache?.[row];
|
||||
}
|
||||
|
||||
@@ -385,6 +385,34 @@ test.describe('Search Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('onBeforeSearch and onAfterSearch', () => {
|
||||
test.beforeEach(async () => {
|
||||
await ctx.page.evaluate(`
|
||||
window.events = [];
|
||||
window.search.onBeforeSearch(() => window.events.push('before'));
|
||||
window.search.onAfterSearch(() => window.events.push('after'));
|
||||
`);
|
||||
});
|
||||
test('should fire before and after findNext', async () => {
|
||||
await ctx.proxy.write('abc');
|
||||
await ctx.page.evaluate(`window.search.findNext('a')`);
|
||||
deepStrictEqual(await ctx.page.evaluate('window.events'), ['before', 'after']);
|
||||
});
|
||||
|
||||
test('should fire before and after findPrevious', async () => {
|
||||
await ctx.proxy.write('abc');
|
||||
await ctx.page.evaluate(`window.search.findPrevious('a')`);
|
||||
deepStrictEqual(await ctx.page.evaluate('window.events'), ['before', 'after']);
|
||||
});
|
||||
|
||||
test('should fire for each search call', async () => {
|
||||
await ctx.proxy.write('abc abc');
|
||||
await ctx.page.evaluate(`window.search.findNext('abc')`);
|
||||
await ctx.page.evaluate(`window.search.findNext('abc')`);
|
||||
deepStrictEqual(await ctx.page.evaluate('window.events'), ['before', 'after', 'before', 'after']);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Regression tests', () => {
|
||||
test.describe('#2444 wrapped line content not being found', () => {
|
||||
let fixture: string;
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+11
-2
@@ -152,8 +152,17 @@ declare module '@xterm/addon-search' {
|
||||
public clearActiveDecoration(): void;
|
||||
|
||||
/**
|
||||
* When decorations are enabled, fires when
|
||||
* the search results change.
|
||||
* Fires after a search is performed.
|
||||
*/
|
||||
readonly onAfterSearch: IEvent<void>;
|
||||
|
||||
/**
|
||||
* Fires before a search is performed.
|
||||
*/
|
||||
readonly onBeforeSearch: IEvent<void>;
|
||||
|
||||
/**
|
||||
* When decorations are enabled, fires when the search results change.
|
||||
*/
|
||||
readonly onDidChangeResults: IEvent<ISearchResultChangeEvent>;
|
||||
}
|
||||
|
||||
@@ -112,13 +112,123 @@ describe('SerializeAddon', () => {
|
||||
}), 'world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('underline styles', () => {
|
||||
it('should serialize single underline with style', async () => {
|
||||
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize double underline', async () => {
|
||||
await writeP(terminal, sgr('4:2') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:2mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize curly underline', async () => {
|
||||
await writeP(terminal, sgr('4:3') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:3mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize dotted underline', async () => {
|
||||
await writeP(terminal, sgr('4:4') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:4mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize dashed underline', async () => {
|
||||
await writeP(terminal, sgr('4:5') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:5mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize underline with RGB color', async () => {
|
||||
await writeP(terminal, sgr('4', '58;2;255;128;64') + 'test' + sgr('24'));
|
||||
const result = serializeAddon.serialize();
|
||||
assert.ok(result.includes('4:1'), result);
|
||||
assert.ok(result.includes('58:2::255:128:64'), result);
|
||||
});
|
||||
|
||||
it('should serialize underline with palette color', async () => {
|
||||
await writeP(terminal, sgr('4', '58;5;46') + 'test' + sgr('24'));
|
||||
const result = serializeAddon.serialize();
|
||||
assert.ok(result.includes('4:1'), result);
|
||||
assert.ok(result.includes('58:5:46'), result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll region', () => {
|
||||
let scrollTerminal: Terminal;
|
||||
let scrollAddon: SerializeAddon;
|
||||
|
||||
beforeEach(() => {
|
||||
scrollTerminal = new Terminal({ cols: 10, rows: 5, allowProposedApi: true });
|
||||
scrollAddon = new SerializeAddon();
|
||||
scrollTerminal.loadAddon(scrollAddon);
|
||||
});
|
||||
|
||||
it('should serialize scroll region when margins are set', async () => {
|
||||
await writeP(scrollTerminal, '\x1b[2;4r');
|
||||
const buffer = (scrollTerminal as any)._core.buffer;
|
||||
assert.equal(buffer.scrollTop, 1, 'scrollTop should be 1');
|
||||
assert.equal(buffer.scrollBottom, 3, 'scrollBottom should be 3');
|
||||
const result = scrollAddon.serialize();
|
||||
assert.ok(result.includes('\x1b[2;4r'), result);
|
||||
});
|
||||
|
||||
it('should not serialize scroll region when excludeModes is true', async () => {
|
||||
await writeP(scrollTerminal, '\x1b[2;4r');
|
||||
const result = scrollAddon.serialize({ excludeModes: true });
|
||||
assert.ok(!result.includes('\x1b[2;4r'), result);
|
||||
});
|
||||
|
||||
it('should restore scroll region correctly when deserialized', async () => {
|
||||
await writeP(scrollTerminal, '\x1b[2;4r');
|
||||
const serialized = scrollAddon.serialize();
|
||||
const terminal2 = new Terminal({ cols: 10, rows: 5, allowProposedApi: true });
|
||||
terminal2.loadAddon(new SerializeAddon());
|
||||
await writeP(terminal2, serialized);
|
||||
const buffer = (terminal2 as any)._core.buffer;
|
||||
assert.equal(buffer.scrollTop, 1);
|
||||
assert.equal(buffer.scrollBottom, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursor visibility', () => {
|
||||
it('should serialize hidden cursor', async () => {
|
||||
await writeP(terminal, 'hello\x1b[?25l');
|
||||
assert.equal(terminal.modes.showCursor, false);
|
||||
const result = serializeAddon.serialize();
|
||||
assert.ok(result.includes('\x1b[?25l'), result);
|
||||
});
|
||||
|
||||
it('should not serialize visible cursor (default state)', async () => {
|
||||
await writeP(terminal, 'hello');
|
||||
assert.equal(terminal.modes.showCursor, true);
|
||||
const result = serializeAddon.serialize();
|
||||
assert.ok(!result.includes('\x1b[?25l'), result);
|
||||
assert.ok(!result.includes('\x1b[?25h'), result);
|
||||
});
|
||||
|
||||
it('should not serialize cursor visibility when excludeModes is true', async () => {
|
||||
await writeP(terminal, 'hello\x1b[?25l');
|
||||
const result = serializeAddon.serialize({ excludeModes: true });
|
||||
assert.ok(!result.includes('\x1b[?25l'), result);
|
||||
});
|
||||
|
||||
it('should restore hidden cursor correctly when deserialized', async () => {
|
||||
await writeP(terminal, 'hello\x1b[?25l');
|
||||
const serialized = serializeAddon.serialize();
|
||||
const terminal2 = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
|
||||
terminal2.loadAddon(new SerializeAddon());
|
||||
await writeP(terminal2, serialized);
|
||||
assert.equal(terminal2.modes.showCursor, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('html', () => {
|
||||
it('empty terminal with selection turned off', () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.notEqual(output, '');
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) ?? []).length, 2);
|
||||
});
|
||||
|
||||
it('empty terminal with no selection', () => {
|
||||
@@ -135,7 +245,7 @@ describe('SerializeAddon', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('basic terminal with html unsafe chars', async () => {
|
||||
@@ -145,7 +255,7 @@ describe('SerializeAddon', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span><a>&pi;<\/span><\/div>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<div><span><a>&pi;<\/span><\/div>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('serializes rows within a provided range', async () => {
|
||||
@@ -157,7 +267,7 @@ describe('SerializeAddon', () => {
|
||||
startCol: 4
|
||||
}
|
||||
});
|
||||
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) || [];
|
||||
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) ?? [];
|
||||
assert.equal(rowMatches.length, 1, output);
|
||||
assert.ok(rowMatches[0]?.includes('hello'));
|
||||
assert.ok(!output.includes('bye'));
|
||||
@@ -168,77 +278,121 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with italic styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with inverse styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with double underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with curly underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dotted underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dashed underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline color (palette)', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4', '58;5;46') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.ok(output.includes('text-decoration: underline;'), output);
|
||||
assert.ok(output.includes('text-decoration-color: #00ff00;'), output);
|
||||
});
|
||||
|
||||
it('cells with underline color (RGB)', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4', '58;2;255;128;64') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.ok(output.includes('text-decoration: underline;'), output);
|
||||
assert.ok(output.includes('text-decoration-color: #ff8040;'), output);
|
||||
});
|
||||
|
||||
it('cells with invisible styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dim styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with strikethrough styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with combined styling', async () => {
|
||||
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) ?? []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) ?? []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with background styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with default options', async () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with custom options', async () => {
|
||||
@@ -251,14 +405,14 @@ describe('SerializeAddon', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with background included', async () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with custom color styling', async () => {
|
||||
@@ -268,7 +422,7 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling - xterm headless', async () => {
|
||||
@@ -278,7 +432,7 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } fro
|
||||
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
||||
import { IAttributeData, IColor } from 'common/Types';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
@@ -82,10 +83,24 @@ function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boole
|
||||
&& cell1.getBgColor() === cell2.getBgColor();
|
||||
}
|
||||
|
||||
function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
// If neither cell has underline, consider them equal regardless of internal underline color
|
||||
// values
|
||||
if (!cell1.isUnderline() && !cell2.isUnderline()) {
|
||||
return true;
|
||||
}
|
||||
const cell1Data = cell1 as unknown as IAttributeData;
|
||||
const cell2Data = cell2 as unknown as IAttributeData;
|
||||
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
|
||||
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
|
||||
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& equalUnderline(cell1, cell2)
|
||||
&& cell1.isOverline() === cell2.isOverline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
@@ -274,7 +289,27 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
if (flagsChanged) {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
|
||||
if (!equalUnderline(cell, oldCell)) {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
const style = cellData.getUnderlineStyle();
|
||||
if (style === UnderlineStyle.NONE) {
|
||||
sgrSeq.push(24);
|
||||
} else {
|
||||
// Use SGR 4:X format for underline styles
|
||||
sgrSeq.push('4:' + style as unknown as number);
|
||||
// Handle underline color
|
||||
if (!cellData.isUnderlineColorDefault()) {
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
|
||||
} else {
|
||||
sgrSeq.push('58:5:' + color as unknown as number);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cell.isUnderline() !== oldCell.isUnderline()) {
|
||||
sgrSeq.push(cell.isUnderline() ? 4 : 24);
|
||||
}
|
||||
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
|
||||
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
|
||||
@@ -422,7 +457,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
export class SerializeAddon implements ITerminalAddon, ISerializeApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -478,6 +513,25 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the scroll region (DECSTBM) if it's not set to the full terminal size.
|
||||
* Uses internal API access since scroll region is not exposed in the public API.
|
||||
*/
|
||||
private _serializeScrollRegion(terminal: Terminal): string {
|
||||
// HACK: Internal API access since scroll region is not exposed in the public API
|
||||
const buffer = (terminal as any)._core.buffer;
|
||||
const scrollTop: number = buffer.scrollTop;
|
||||
const scrollBottom: number = buffer.scrollBottom;
|
||||
|
||||
// Only serialize if scroll region is not the default (full terminal size)
|
||||
if (scrollTop !== 0 || scrollBottom !== terminal.rows - 1) {
|
||||
// DECSTBM uses 1-based indices: CSI Ps ; Ps r
|
||||
return `\x1b[${scrollTop + 1};${scrollBottom + 1}r`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private _serializeModes(terminal: Terminal): string {
|
||||
let content = '';
|
||||
const modes = terminal.modes;
|
||||
@@ -505,6 +559,12 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor visibility (DECTCEM)
|
||||
// Default: visible
|
||||
if (!modes.showCursor) {
|
||||
content += '\x1b[?25l';
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -527,9 +587,10 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
}
|
||||
}
|
||||
|
||||
// Modes
|
||||
// Modes and scroll region
|
||||
if (!options?.excludeModes) {
|
||||
content += this._serializeModes(this._terminal);
|
||||
content += this._serializeScrollRegion(this._terminal);
|
||||
}
|
||||
|
||||
return content;
|
||||
@@ -540,7 +601,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
return this._serializeBufferAsHTML(this._terminal, options || {});
|
||||
return this._serializeBufferAsHTML(this._terminal, options ?? {});
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
@@ -569,20 +630,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
targetLength = targetLength >> 0;
|
||||
padString = padString ?? ' ';
|
||||
if (target.length > targetLength) {
|
||||
return target;
|
||||
}
|
||||
|
||||
targetLength -= target.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length);
|
||||
}
|
||||
return padString.slice(0, targetLength) + target;
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
||||
|
||||
@@ -619,7 +666,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
||||
return this._ansiColors[color].css;
|
||||
@@ -627,6 +674,42 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getUnderlineColor(cell: IBufferCell): string | undefined {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
if (cellData.isUnderlineColorDefault()) {
|
||||
return undefined;
|
||||
}
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
// Palette color
|
||||
return this._ansiColors[color].css;
|
||||
}
|
||||
|
||||
private _getUnderlineStyle(cell: IBufferCell): string {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
switch (cellData.getUnderlineStyle()) {
|
||||
case UnderlineStyle.SINGLE:
|
||||
return 'underline';
|
||||
case UnderlineStyle.DOUBLE:
|
||||
return 'underline double';
|
||||
case UnderlineStyle.CURLY:
|
||||
return 'underline wavy';
|
||||
case UnderlineStyle.DOTTED:
|
||||
return 'underline dotted';
|
||||
case UnderlineStyle.DASHED:
|
||||
return 'underline dashed';
|
||||
default:
|
||||
return 'underline';
|
||||
}
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
@@ -647,14 +730,36 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
|
||||
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
|
||||
// Handle text-decoration (underline, overline, strikethrough, blink)
|
||||
const decorations: string[] = [];
|
||||
if (cell.isUnderline()) {
|
||||
decorations.push(this._getUnderlineStyle(cell));
|
||||
}
|
||||
if (cell.isOverline()) {
|
||||
decorations.push('overline');
|
||||
}
|
||||
if (cell.isStrikethrough()) {
|
||||
decorations.push('line-through');
|
||||
}
|
||||
if (cell.isBlink()) {
|
||||
decorations.push('blink');
|
||||
}
|
||||
if (decorations.length > 0) {
|
||||
content.push('text-decoration: ' + decorations.join(' ') + ';');
|
||||
}
|
||||
|
||||
// Handle underline color
|
||||
if (cell.isUnderline()) {
|
||||
const underlineColor = this._getUnderlineColor(cell);
|
||||
if (underlineColor) {
|
||||
content.push('text-decoration-color: ' + underlineColor + ';');
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
if (cell.isDim()) { content.push('opacity: 0.5;'); }
|
||||
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,19 +9,15 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
||||
import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes';
|
||||
import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
|
||||
|
||||
export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi {
|
||||
export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi {
|
||||
private _provider15Graphemes?: UnicodeGraphemeProvider;
|
||||
private _provider15?: UnicodeGraphemeProvider;
|
||||
private _unicode?: IUnicodeHandling;
|
||||
private _oldVersion: string = '';
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (! this._provider15) {
|
||||
this._provider15 = new UnicodeGraphemeProvider(false);
|
||||
}
|
||||
if (! this._provider15Graphemes) {
|
||||
this._provider15Graphemes = new UnicodeGraphemeProvider(true);
|
||||
}
|
||||
this._provider15 ??= new UnicodeGraphemeProvider(false);
|
||||
this._provider15Graphemes ??= new UnicodeGraphemeProvider(true);
|
||||
const unicode = terminal.unicode;
|
||||
this._unicode = unicode;
|
||||
unicode.register(this._provider15);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import UnicodeTrie from './unicode-trie';
|
||||
const trieRaw = "AAARAAAAAABwxwAAAb4LQfTtmw+sVmUdx58LL/ffe/kjzNBV80gW1F3yR+6CvbJiypoZa0paWmAWSluErSBbFtYkkuZykq6QamGJ4WRqo2kFGy6dYWtEq6G1MFAJbRbOVTQr+x7f5+x97q/n/3me87wXzm/3s+f/7/d7/p7znnvOlvGMbQM7wIPgEbAPHABPgcPgefAS+BfYwuv/F/Q2OulBxKcK6TMRPxu8FcwFbwcjYCFYDC4Cl4ArwNXgGvBJsA58UdBDwy+jbBO4La8DtoEd4H7wkNBuN+KPgn3gADgIngaHwFHwF/AyeAWMm4C+TGi3LdiJ/EnIex04A2RgFpgD5oKFYDG4CLwHXAo+IKSvAqt4/evA9bz9jWA6+Cq3dyvCP8HWNwX93wF38/ROcD94SCjP2+1B+BiPP4HwgOD/7xD/I08fRniMx48jPAFeBeuF+n29jE0G08FZvaPHYWZvh9mcEfAOjlhXx/qGfd2QvLO3zccmtMnzliC9lPt+GenD1nyMiK/LNf1cycs+gfAzPJ6vtxe4jhuQtx5sBLeA28G3eb3v8/Beif4HkPewxu5G6N/rMP4qfgEdvwZPgj+AZ8Cx3nYfxiE8Dk6AV0FfH/YEOB28AbwJDIPzQAtcAC4Gl/Z19F+J+NVCehWPr0b46b7RvixvdPg8yr7U10l/BfFN4La8DdgGdoAHwU/AI2AfOACeAofB8+AlcAKwfvyBKeCM/o7NrF9PXmdWv9/Ynot2I7ztIg8dF5I2a8i63CjZU+9Fm2Wcy4U4ZQVYyeOrwVoev57UuxHcJKRvFuJXgnU8/nUebtbYrKmpCUOx31P7UVNTU1NTU1NTU1OGLTz8Xr/77+W7+9vP0or0MxPMbXaizY8FW3sQ3wseB/t5/kGEh8DR/vbzwL8i/Af4Dy8fP8BYE0weaKenI/wV/DhrQG97JspngzlgLpgHzgPzwUhdVpfVZXVZXRa87HxwAVgQ4Pn5WEd85l5TUzOasvezFw/E3b/LoP9D4CpwrcTWWsGXNQOj748/G9k3G56d1KYxmbELwQbwKFiJvBM8nDWlHa5E+AOwCzwLzjkNeeB28NvTeB1OYyr0gQ1g99R23nGE50xj7MPgc+A+8K5Bxj4FHgB/G2z/T9XEzCZjd/S0WYX4Pc3/r/Nn5I0f6qQXIP5x8ENwBMyYyNhHJ3b0pOCuLrBvM941NTU1JyNHEp+BrC8dMyalt1/m3uWfhmeULzRGp9d3wf0WZSN8+prCr60Wz09tuNmx35sl9Y825HXvRN39KNveaL8flb9f913kbec67kHeTsR3gYcH2uV7ED4m2HhCYi/X9ZuBzvuXv0f8iKIfx5B/XCg7gTgbVPdvAsomCuWnD45eK28UyvL3Jt+s0fU2TVnOXJQvJHUWIb0ELAWXgCt4+UcMumSsEtpch/g6ouMGpG/ieZsc9N/q4YsLd3D9WyPbsWEbfNgO7hN82TWY/n8xKbmsC3xQsYKf+7sjrx2TH+u4H3vhx+OO6+X9hmtXN7C/4r15EPaeBs9J7L7YBeeED/k7wn8fbIf/Rji+yVizmd4vW6bB19cb/PU9w7MxMA60bzPHgM8+zG623+OnzOf55yNc3Gw/k303wveBy3nZcoTXgNVgLfiCRNcG5N3SbIebwZ08fhe4l8d/BH7K4yI/4+HPwS/BAfBks+PzIaHuc3x+ivSL4GUyZ68I6fwZYRNMG2qnz+Th2QjfMtTx/1zE5w61nyN+Q7C3aKgdin1dgrylYBn4INdhGn/Z2FfFiqH01/SUXMvnPD+jC+j85N/RqRhR/DYaS6T+P09K1mD+vzW+5zVqqeVUl0wTz2lK8odJHRGXfBufdGLSoSo3+ZFJ6sl0qvJVNmhI4z4i06mrZ6uT1le1z5h5HE3tMiHPtQ5javu+ItMXUr/MXpmwmyRL3D6U7UwIMyYfczGu0qdqb2pbhcw4xQkhWQBMerrZ/liXrGTbsQwTwrEu4zSczKLrd7fCSKiKn+zSo8BWXMe8myXWOivrUxWi60OPoQ7VIasbQ0S/Ukk3rZVullNhHEL1rYoxUF0PTfm6elWJzq54ZsU4z11ohOy0oxT2izFqCNj4TesXcWZo6+Jfqr1O+1O1beqDagypj2J9F1u2daucj3Eknmq/6PaHrK7Mb1o35DiW1a/a76LuhlDXZX25SOz11S33ErKxDb2/fc/bFKI6axskn+4/W90u9mOtbRf7smsoTdvOfwoRz0t6DaP9k81v6P7Re5aUQudTd303rX+bZzBl97/KR7E+Xbux9lLI+aNr1PfaYLpPDiW2/vrYTX1drMIeXbMye6HXlw8292Jl7ZXxLxRlxXbcaH9drjFlxfa3Qozx8NWRi834lPVZbD+SmN7EJPzc9TVCSVXXDps9L+513b2J7fMu176V2YOhx1A3JrJ8KrLxUumpcu5j/lYT+2tzLRVDZmhjO442a1Clu0ox9VPVXzE/lcS4V0k1D6LI1pJsz8fct9SGbO5l/rmKzTlvsxdj3IvRtC2uv0t1fotltvd2VaCy5Sp5m0EhnZG4CCNxXZrWp/VUIrOjapfnNw11ZNI0V/GWzKNuxtzGKKTEtJeR0NVmpojbtBuW5On0u0is9ZMxvU8ZM+8vEyadtu10oqtP9Q4rcJEm85+Two/QkpGwjI6YkgkhtUfzZOW6fFVexuRri+qj9TJJHZkdmW5abiu0rs6uj2TMfmx06bISUj9tZ9Lja8dVQtox6WpxTJKfW3M4MSTmvU4sWy1CU6BF4jIfdNeDjHWuO1lCWIm2Jr2ixNZvklD2fP0Q6+vsmO4hqN1hJvfDtV5G8mTlsvau4qPP1a64L1skT6QYEzEtq0PzGZOfCbSdSmcKTP7Qs86Ej/1hEpelaV6IMdT5ayu2+nT9tmnnO746XbLxE8t0qOrYtJWhmk9bvaLfsrotRVw1PnR+bcafSUKZ6Mps7smobybJLH2R6WqRkJa1DHV0UmbfUcksiSF0HExSpp+uY0zbTklMaCm7blzEtg8h1rNMXNaYi05ZXsbC75sQ/4+aUxFV2jL50Q3jE0rK2rVtN09By8OHoo1vH2LPSdE323mr2sdu0pUZiDkWLRKWnfeQY6taKzHF9n/GPv8jd/0/egiRvYMR24fU79iY3s9Qva9RlYR8n8HHtq9fMcT1HRWfdZXiHd9YInt/iI4PTaf+BimXKvdXYU+3hlRpHzs2dVK/cxhDn+xs0I2jzxjL5kpXz1VU72aLtkK/97sALKyQqu25SshvG6h08/cLrlKswRklKXvvXfa+pZt+y8nah5YUv2Oo/ap/X2URdRfico9K69hcp6r6XaCz5Wo/hs/iNTGF6N6tV92/9ZS0Wba9SlT3pKF/e6W674+x9ly+VRL73cPU8ygb31D3eSqfVd+iqET0y3YMYojoO11XqrTt2nPxmeq1HYeqxkmUMt8DiesjpoTSr+qDrD+qPZDiOZxMdH0pRPX8MFUfQtv0Xbs+a1a1NnRryNZ/2+tsaPG5ZoX0RXZei88yZGdo4UMPj/cwv/kMJboxLISuQbE+1VW12Mx7FWOrW3M9Hv7Y+uxyraPSo8B2TGPuLdOeZha+hBKf8Sjsm/oR+7pmsx/oeOraFWdXleeV6oyl41zm+mgSuq9C6ox1TsU8D+m4dwMmf8v2nz7Tm+fYfj7HV1K/x1HWjquvY+2dllxM64ue87Su772zzbXIVC+WxLZTRR9MdkMTypZNH1z6G0tUvoccwxA+hfLNdV+a7MaQqscztMi+7QnxDZXvd1dldWQOyMbApb1Jd2h91Ffx+y9Xfb7tClokboOvrRhrbVpFFO8z+65t2/u4su9MUx028znH01/TGVDmHAj13W1o+1USw+eUfYtpO+b82rRNsb6oPpV+1fdBqddB6n3WDXvdJDZrJ0QfQp6bsc/kqq4BIddHWXGdN1pmWveh58F1zYUW1zmOITHXWOg1XrZvZSWUf77tq1ofqear6muaT1lIQp3bofabSafJVlnfYo9B6LGr8uzz2Xchvzfw+T9PlgiV/A8=";
|
||||
|
||||
declare const Buffer: any;
|
||||
declare const Buffer: { from(s: string, encoding: string): Uint8Array } | undefined;
|
||||
function _dec(s: string): Uint8Array {
|
||||
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
|
||||
const bs = atob(s);
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11';
|
||||
import { UnicodeV11 } from './UnicodeV11';
|
||||
|
||||
export class Unicode11Addon implements ITerminalAddon , IUnicode11Api {
|
||||
export class Unicode11Addon implements ITerminalAddon, IUnicode11Api {
|
||||
public activate(terminal: Terminal): void {
|
||||
terminal.unicode.register(new UnicodeV11());
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2024, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user