buninu 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
- package/apps/jsgotty/NOTICE +5 -0
- package/apps/jsgotty/README.en.md +21 -0
- package/apps/jsgotty/README.md +21 -0
- package/apps/jsgotty/client-modes.mjs +117 -0
- package/apps/jsgotty/client.mjs +100 -10
- package/apps/jsgotty/clipboard-powershell.js +18 -0
- package/apps/jsgotty/clipboard.js +148 -0
- package/apps/jsgotty/gotty.js +13 -0
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +1 -1
- package/apps/jsgotty/static/css/index.css +14 -1
- package/apps/jsgotty/static/js/gotty.js +107 -8
- package/apps/jsgotty/static/js/kitty.js +115 -7
- package/apps/jsgotty/terminal-modes.js +332 -0
- package/apps/jsgotty/tests/client-exit.test.mjs +45 -0
- package/apps/jsgotty/tests/client-modes.test.mjs +105 -0
- package/apps/jsgotty/tests/clipboard-browser.test.js +81 -0
- package/apps/jsgotty/tests/clipboard.test.js +75 -0
- package/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
- package/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
- package/apps/jsgotty/tests/manual-touch-diagnostics.js +33 -0
- package/apps/jsgotty/tests/manual-touch-diagnostics.mjs +22 -0
- package/apps/jsgotty/tests/nested-cli-pty.test.mjs +116 -0
- package/apps/jsgotty/tests/reconnect-modes-browser.test.js +144 -0
- package/apps/jsgotty/tests/reconnect-modes.md +44 -0
- package/apps/jsgotty/tests/reconnect-modes.test.js +166 -0
- package/apps/jsgotty/tests/touch-drag-browser.test.js +106 -0
- package/apps/jsgotty/tests/wheel-browser.test.js +60 -0
- package/apps/jsmdcui/CHANGELOG.md +44 -0
- package/apps/jsmdcui/package.json +1 -1
- package/apps/jsmdcui/src/highlight/go-regexp.js +83 -0
- package/apps/jsmdcui/src/highlight/highlighter.js +49 -62
- package/apps/jsmdcui/src/highlight/parser.js +8 -5
- package/apps/jsmdcui/tests/go-regexp.test.js +98 -0
- package/apps/jsmdcui/tests/heading-ids.test.js +21 -16
- package/apps/jsmdcui/tests/highlight-regions.test.js +73 -0
- package/apps/jsmdcui/tests/id-collision.test.js +32 -9
- package/apps/jsmdcui/tests/markdown-behavior.mjs +9 -0
- package/apps/jsmdcui/tests/syntax-parity/NORMAL-HTML.md +46 -0
- package/apps/jsmdcui/tests/syntax-parity/README.md +85 -0
- package/apps/jsmdcui/tests/syntax-parity/REPORT.md +328 -0
- package/apps/jsmdcui/tests/syntax-parity/compare.mjs +75 -0
- package/apps/jsmdcui/tests/syntax-parity/go.mod +5 -0
- package/apps/jsmdcui/tests/syntax-parity/normal-html-report.json +619 -0
- package/apps/jsmdcui/tests/syntax-parity/normal-html.html +9 -0
- package/apps/jsmdcui/tests/syntax-parity/oracle.go +49 -0
- package/apps/jsmdcui/tests/syntax-parity/report.json +2174 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/PowerShell.txt +39 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ada.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/apacheconf.txt +49 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/arduino.txt +38 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/asciidoc.txt +33 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/asm.txt +77 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ats.txt +48 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/awk.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/b.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/bat.txt +39 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/c.txt +34 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/caddyfile.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cake.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/clojure.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cmake.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/coffeescript.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/colortest.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/conky.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cpp.txt +46 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/crontab.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/crystal.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/csharp.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/css.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/csx.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cuda.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cython.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/d.txt +52 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dart.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/default.txt +8 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dockerfile.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dot.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/elixir.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/elm.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/erb.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/erlang.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fish.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/forth.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fortran.txt +40 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/freebsd-kernel.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fsharp.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gdscript.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gemini.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gentoo-ebuild.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gentoo-etc-portage.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-commit.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-config.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-rebase-todo.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gleam.txt +44 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/glsl.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gnuplot.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/go.txt +33 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/godoc.txt +9 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/golo.txt +50 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gomod.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/graphql.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/groff.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/groovy.txt +42 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/haml.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/hare.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/haskell.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/hc.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html4.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html5.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ini.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/inputrc.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/java.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/javascript.txt +41 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/jinja2.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/json.txt +21 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/jsonnet.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/julia.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/justfile.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/keymap.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kickstart.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kotlin.txt +30 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kvlang.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ledger.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lfe.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lilypond.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lisp.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/log.txt +45 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lua.txt +48 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mail.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/makefile.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/man.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/markdown.txt +21 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mc.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/meson.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/micro.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mpdconf.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/msbuild.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nanorc.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nftables.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nginx.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nim.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nix.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nu.txt +92 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/objc.txt +36 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ocaml.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/octave.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/odin.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pascal.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/patch.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/peg.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/perl.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/php.txt +44 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pkg-config.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/po.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pony.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pov.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-action.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-config.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-filter.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/proto.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/prql.txt +42 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/puppet.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/python2.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/python3.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/r.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/raku.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/reST.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/renpy.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/rpmspec.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ruby.txt +34 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/rust.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sage.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/scad.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/scala.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sed.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sh.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sls.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/smalltalk.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/solidity.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sql.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/stata.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/svelte.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/swift.txt +53 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/systemd.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/tcl.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/terraform.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/tex.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/toml.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/twig.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/typescript.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/v.txt +47 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vala.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/verilog.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vhdl.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vi.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vue.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/xml.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/xresources.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/yaml.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/yum.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zig.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zscript.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zsh.txt +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Opt-in real bundled-xterm integration; no browser/dependency downloads.
|
|
2
|
+
// CHROMIUM_HEADLESS=/path/to/chromium-headless-shell node --test tests/touch-drag-browser.test.js
|
|
3
|
+
const test=require('node:test');
|
|
4
|
+
const assert=require('node:assert/strict');
|
|
5
|
+
const fs=require('node:fs');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const http=require('node:http');
|
|
8
|
+
const {spawn}=require('node:child_process');
|
|
9
|
+
const {once}=require('node:events');
|
|
10
|
+
const WebSocket=require('ws');
|
|
11
|
+
const root=path.join(__dirname,'../static');
|
|
12
|
+
test('touch double-tap drag and image-menu right click use bundled xterm', {skip:!process.env.CHROMIUM_HEADLESS,timeout:60000},async()=>{
|
|
13
|
+
const bundle=fs.readFileSync(path.join(root,'js/gotty.js'),'utf8').replace('(i.nc = void 0));','(i.nc = void 0)); window.TestGoTTYXterm = i(634).GoTTYXterm;');
|
|
14
|
+
const server=http.createServer((req,res)=>{
|
|
15
|
+
if(req.url==='/index.css'){res.setHeader('Content-Type','text/css');res.end(fs.readFileSync(path.join(root,'css/index.css')));}
|
|
16
|
+
else if(req.url==='/bundle.js'){res.setHeader('Content-Type','text/javascript');res.end(bundle);}
|
|
17
|
+
else if(req.url==='/gestures.js'){
|
|
18
|
+
const source=fs.readFileSync(path.join(root,'js/kitty.js'),'utf8');
|
|
19
|
+
res.setHeader('Content-Type','text/javascript');
|
|
20
|
+
res.end(source.slice(source.indexOf(' function reportTouchMouse('),source.indexOf(' function ensureContextMenu('))+source.slice(source.indexOf(' function ensureContextMenu('),source.indexOf(' function bindImageContextMenu('))+source.slice(source.indexOf(' function bindImageContextMenu('),source.indexOf(' function bindImageContextMenu(')+source.slice(source.indexOf(' function bindImageContextMenu(')).indexOf('\n function ',5))+'\nbindTouchScrolling();bindImageContextMenu();');
|
|
21
|
+
}
|
|
22
|
+
else if(req.url==='/keyboard.js'){res.setHeader('Content-Type','text/javascript');res.end(fs.readFileSync(path.join(root,'js/kitty-keyboard.js')));}
|
|
23
|
+
else res.end('<!doctype html><div id="terminal"><div id="fixture" style="width:800px;height:600px"></div></div><link rel="stylesheet" href="/index.css"><script src="/keyboard.js"></script><script src="/bundle.js"></script><script>window.wrapper=new TestGoTTYXterm(document.getElementById("fixture"));window.sent=[];wrapper.onInput(b=>sent.push(new TextDecoder().decode(b)));wrapper.kittyKeyboard.android=true;</script>');
|
|
24
|
+
});
|
|
25
|
+
server.listen(0,'127.0.0.1');await once(server,'listening');
|
|
26
|
+
const child=spawn(process.env.CHROMIUM_HEADLESS,['--no-sandbox','--disable-gpu','--remote-debugging-port=0','http://127.0.0.1:'+server.address().port],{detached:true,stdio:['ignore','ignore','pipe']});
|
|
27
|
+
let ws;
|
|
28
|
+
try{
|
|
29
|
+
const endpoint=await new Promise((resolve,reject)=>{let log='';const timer=setTimeout(()=>reject(Error('Browser startup timeout: '+log)),15000);child.on('error',reject);child.on('exit',code=>{clearTimeout(timer);reject(Error('Browser exited '+code+': '+log));});child.stderr.on('data',b=>{log+=b;const m=log.match(/DevTools listening on (ws:\/\/[^\s]+)/);if(m){clearTimeout(timer);resolve(m[1]);}});});
|
|
30
|
+
const u=new URL(endpoint);let tab;
|
|
31
|
+
for(let i=0;i<50;i++){tab=(await (await fetch('http://'+u.host+'/json/list')).json()).find(x=>x.type==='page');if(tab)break;await new Promise(r=>setTimeout(r,100));}
|
|
32
|
+
ws=new WebSocket(tab.webSocketDebuggerUrl);await once(ws,'open');let id=0;const pending=new Map();ws.on('message',data=>{const m=JSON.parse(data);if(pending.has(m.id)){const {resolve,reject}=pending.get(m.id);pending.delete(m.id);m.error?reject(m.error):resolve(m.result);}});
|
|
33
|
+
const call=(method,params)=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
|
|
34
|
+
for(let i=0;i<50;i++){const r=await call('Runtime.evaluate',{expression:'Boolean(window.wrapper)',returnByValue:true});if(r.result.value)break;await new Promise(r=>setTimeout(r,100));}
|
|
35
|
+
const result=await call('Runtime.evaluate',{awaitPromise:true,returnByValue:true,expression:`(async()=>{
|
|
36
|
+
const term=wrapper.term;
|
|
37
|
+
const write=s=>new Promise(r=>term.write(s,r));
|
|
38
|
+
const equal=(a,b)=>{if(JSON.stringify(a)!==JSON.stringify(b))throw Error(JSON.stringify({actual:a,expected:b}));};
|
|
39
|
+
await write('\\x1b[?1002h\\x1b[?1006h');
|
|
40
|
+
const terminal=document.getElementById('fixture');terminal.__gottyXterm=term;
|
|
41
|
+
const screen=term.element.querySelector('.xterm-screen'), rect=screen.getBoundingClientRect();
|
|
42
|
+
const state={placements:new Map()};
|
|
43
|
+
const xtermElements=()=>({screen});
|
|
44
|
+
const DEFAULT_TOUCH_SCROLL_THRESHOLD=24;
|
|
45
|
+
const removePlacementNode=node=>node.remove();
|
|
46
|
+
eval(await (await fetch('/gestures.js')).text());
|
|
47
|
+
const touch=(type,x=30,y=30,count=1)=>{
|
|
48
|
+
const point=new Touch({identifier:1,target:screen,clientX:rect.left+x,clientY:rect.top+y});
|
|
49
|
+
const touches=type==='touchend'||type==='touchcancel'?[]:count===1?[point]:[point,new Touch({identifier:2,target:screen,clientX:rect.left+x+30,clientY:rect.top+y})];
|
|
50
|
+
const event=new TouchEvent(type,{bubbles:true,cancelable:true,touches,changedTouches:[point]});
|
|
51
|
+
screen.dispatchEvent(event);return event.defaultPrevented;
|
|
52
|
+
};
|
|
53
|
+
const codes=()=>sent.map(s=>{const m=s.match(/<([0-9]+);[0-9]+;[0-9]+([Mm])/);return m?m[1]+m[2]:s;});
|
|
54
|
+
let focusCalls=0;const originalFocus=term._core.focus;term._core.focus=function(...args){focusCalls++;return originalFocus.apply(this,args);};
|
|
55
|
+
const firstTap=()=>{touch('touchstart');touch('touchend');
|
|
56
|
+
// Browser compatibility mouse sequence after an ordinary tap.
|
|
57
|
+
for(const type of ['mousedown','mouseup'])screen.dispatchEvent(new MouseEvent(type,{bubbles:true,clientX:rect.left+30,clientY:rect.top+30,button:0,buttons:type==='mousedown'?1:0}));};
|
|
58
|
+
sent.length=0;firstTap();equal(codes(),['0M','0m']);
|
|
59
|
+
const focusBeforeDrag=focusCalls;
|
|
60
|
+
if(!touch('touchstart'))throw Error('Second press must suppress compatibility click');
|
|
61
|
+
equal(codes(),['0M','0m','0M']); // still held, no premature release
|
|
62
|
+
touch('touchmove',80,70);touch('touchend',80,70);
|
|
63
|
+
equal(codes(),['0M','0m','0M','32M','0m']);
|
|
64
|
+
equal(focusCalls,focusBeforeDrag);
|
|
65
|
+
sent.length=0;firstTap();touch('touchstart');touch('touchend');
|
|
66
|
+
equal(codes(),['0M','0m','0M','0m']); // ordinary double click
|
|
67
|
+
for(const cancel of [()=>touch('touchcancel'),()=>touch('touchstart',30,30,2),()=>window.dispatchEvent(new Event('blur')),()=>state.cancelTouchGesture()]){
|
|
68
|
+
sent.length=0;firstTap();touch('touchstart');cancel();touch('touchend');equal(codes(),['0M','0m','0M','0m']);
|
|
69
|
+
}
|
|
70
|
+
sent.length=0;firstTap();touch('touchstart');
|
|
71
|
+
touch('touchmove',-200,-200);touch('touchend',-200,-200);
|
|
72
|
+
equal(codes(),['0M','0m','0M','32M','0m']); // release outside grid still reaches application
|
|
73
|
+
sent.length=0;touch('touchstart');touch('touchmove',30,90);touch('touchend',30,90);
|
|
74
|
+
if(!codes().length||codes().some(x=>!x.startsWith('64')))throw Error('Single touch must remain scrolling: '+codes());
|
|
75
|
+
sent.length=0;touch('touchstart');touch('touchmove',30,90);touch('touchend',30,90);
|
|
76
|
+
if(codes().some(x=>x==='0M'))throw Error('Scroll must not arm double tap');
|
|
77
|
+
// Use precisely the existing image context-menu path, with its original location.
|
|
78
|
+
const node=document.createElement('div');node.style.zIndex='0';node.getBoundingClientRect=()=>({left:0,top:0,right:1000,bottom:1000});state.placements.set('image',node);
|
|
79
|
+
screen.dispatchEvent(new MouseEvent('contextmenu',{bubbles:true,cancelable:true,clientX:rect.left+30,clientY:rect.top+30,button:2}));
|
|
80
|
+
if(state.contextMenu.root.hidden)throw Error('Menu missing');
|
|
81
|
+
const focusBeforeRight=focusCalls;
|
|
82
|
+
sent.length=0;state.contextMenu.rightButton.click();equal(focusCalls,focusBeforeRight);equal(codes(),['2M','2m']);
|
|
83
|
+
if(!state.contextMenu.root.hidden||!state.placements.has('image'))throw Error('Right click must hide menu and preserve image');
|
|
84
|
+
await write('\\x1b[?1002l');state.cancelTouchGesture();sent.length=0;
|
|
85
|
+
firstTap();touch('touchstart');touch('touchmove',80,70);touch('touchend');
|
|
86
|
+
if(codes().some(x=>/^[032][Mm]$/.test(x)))throw Error('No application mouse reporting outside mouse mode');
|
|
87
|
+
state.cancelTouchGesture();state.placements.clear();
|
|
88
|
+
term.focus();const textarea=term.textarea;
|
|
89
|
+
screen.dispatchEvent(new MouseEvent('contextmenu',{bubbles:true,cancelable:true,clientX:rect.left+30,clientY:rect.top+30,button:2}));
|
|
90
|
+
if(document.activeElement!==textarea)throw Error('Context menu must keep textarea focused for native paste');
|
|
91
|
+
const data=new DataTransfer();data.setData('text/plain','paste-check');sent.length=0;
|
|
92
|
+
textarea.dispatchEvent(new ClipboardEvent('paste',{bubbles:true,cancelable:true,clipboardData:data}));
|
|
93
|
+
if(!sent.join('').includes('paste-check'))throw Error('Paste stopped working');
|
|
94
|
+
equal(getComputedStyle(textarea).touchAction,'none');
|
|
95
|
+
equal(getComputedStyle(screen).touchAction,'manipulation');
|
|
96
|
+
touch('touchstart');
|
|
97
|
+
if(document.activeElement!==textarea)throw Error('Touch must not blur the input');
|
|
98
|
+
touch('touchend');
|
|
99
|
+
wrapper.close();return 'ok';
|
|
100
|
+
})()`});
|
|
101
|
+
assert.equal(result.exceptionDetails,undefined,JSON.stringify(result.exceptionDetails));assert.equal(result.result.value,'ok');
|
|
102
|
+
}finally{
|
|
103
|
+
ws?.close();if(child.pid){try{process.kill(-child.pid,'SIGTERM');}catch{}}
|
|
104
|
+
server.closeAllConnections();await new Promise(r=>server.close(r));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Opt-in real bundled-xterm integration; no browser/dependency downloads.
|
|
2
|
+
// CHROMIUM_HEADLESS=/path/to/chromium-headless-shell node --test tests/kitty-keyboard-browser.test.js
|
|
3
|
+
const test=require('node:test');
|
|
4
|
+
const assert=require('node:assert/strict');
|
|
5
|
+
const fs=require('node:fs');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const http=require('node:http');
|
|
8
|
+
const {spawn}=require('node:child_process');
|
|
9
|
+
const {once}=require('node:events');
|
|
10
|
+
const WebSocket=require('ws');
|
|
11
|
+
const root=path.join(__dirname,'../static');
|
|
12
|
+
test('shipped xterm reports horizontal, vertical and diagonal wheel events', {skip:!process.env.CHROMIUM_HEADLESS,timeout:60000},async()=>{
|
|
13
|
+
const bundle=fs.readFileSync(path.join(root,'js/gotty.js'),'utf8').replace('(i.nc = void 0));','(i.nc = void 0)); window.TestGoTTYXterm = i(634).GoTTYXterm;');
|
|
14
|
+
const server=http.createServer((req,res)=>{
|
|
15
|
+
if(req.url==='/bundle.js'){res.setHeader('Content-Type','text/javascript');res.end(bundle);}
|
|
16
|
+
else if(req.url==='/keyboard.js'){res.setHeader('Content-Type','text/javascript');res.end(fs.readFileSync(path.join(root,'js/kitty-keyboard.js')));}
|
|
17
|
+
else res.end('<!doctype html><div id="fixture" style="width:800px;height:600px"></div><script src="/keyboard.js"></script><script src="/bundle.js"></script><script>window.wrapper=new TestGoTTYXterm(document.getElementById("fixture"));window.sent=[];wrapper.onInput(b=>sent.push(new TextDecoder().decode(b)));wrapper.kittyKeyboard.android=true;</script>');
|
|
18
|
+
});
|
|
19
|
+
server.listen(0,'127.0.0.1');await once(server,'listening');
|
|
20
|
+
const child=spawn(process.env.CHROMIUM_HEADLESS,['--no-sandbox','--disable-gpu','--remote-debugging-port=0','http://127.0.0.1:'+server.address().port],{detached:true,stdio:['ignore','ignore','pipe']});
|
|
21
|
+
let ws;
|
|
22
|
+
try{
|
|
23
|
+
const endpoint=await new Promise((resolve,reject)=>{let log='';const timer=setTimeout(()=>reject(Error('Browser startup timeout: '+log)),15000);child.on('error',reject);child.on('exit',code=>{clearTimeout(timer);reject(Error('Browser exited '+code+': '+log));});child.stderr.on('data',b=>{log+=b;const m=log.match(/DevTools listening on (ws:\/\/[^\s]+)/);if(m){clearTimeout(timer);resolve(m[1]);}});});
|
|
24
|
+
const u=new URL(endpoint);let tab;
|
|
25
|
+
for(let i=0;i<50;i++){tab=(await (await fetch('http://'+u.host+'/json/list')).json()).find(x=>x.type==='page');if(tab)break;await new Promise(r=>setTimeout(r,100));}
|
|
26
|
+
ws=new WebSocket(tab.webSocketDebuggerUrl);await once(ws,'open');let id=0;const pending=new Map();ws.on('message',data=>{const m=JSON.parse(data);if(pending.has(m.id)){const {resolve,reject}=pending.get(m.id);pending.delete(m.id);m.error?reject(m.error):resolve(m.result);}});
|
|
27
|
+
const call=(method,params)=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
|
|
28
|
+
for(let i=0;i<50;i++){const r=await call('Runtime.evaluate',{expression:'Boolean(window.wrapper)',returnByValue:true});if(r.result.value)break;await new Promise(r=>setTimeout(r,100));}
|
|
29
|
+
const result=await call('Runtime.evaluate',{awaitPromise:true,returnByValue:true,expression:`(async()=>{
|
|
30
|
+
const term=wrapper.term;
|
|
31
|
+
const write=s=>new Promise(r=>term.write(s,r));
|
|
32
|
+
const equal=(a,b)=>{if(JSON.stringify(a)!==JSON.stringify(b))throw Error(JSON.stringify({actual:a,expected:b}));};
|
|
33
|
+
await write('\\x1b[?1002h\\x1b[?1006h');
|
|
34
|
+
const screen=term.element.querySelector('.xterm-screen'), rect=screen.getBoundingClientRect();
|
|
35
|
+
const wheel=(deltaX,deltaY,mods={})=>{
|
|
36
|
+
sent.length=0;
|
|
37
|
+
const event=new WheelEvent('wheel',{clientX:rect.left+20,clientY:rect.top+20,deltaX,deltaY,deltaMode:1,bubbles:true,cancelable:true,...mods});
|
|
38
|
+
screen.dispatchEvent(event);
|
|
39
|
+
if(term._core.coreMouseService.areMouseEventsActive&&!event.defaultPrevented)throw Error('Wheel default was not prevented');
|
|
40
|
+
return sent.map(s=>Number(s.match(/<([0-9]+);/)[1]));
|
|
41
|
+
};
|
|
42
|
+
equal(wheel(-1,0),[66]);equal(wheel(1,0),[67]);
|
|
43
|
+
equal(wheel(0,-1,{shiftKey:true}),[68]);equal(wheel(0,1,{shiftKey:true}),[69]);
|
|
44
|
+
equal(wheel(0,-1),[64]);equal(wheel(0,1),[65]);
|
|
45
|
+
equal(wheel(1,-1),[67,64]);
|
|
46
|
+
equal(wheel(-1,0,{shiftKey:true,altKey:true,ctrlKey:true}),[94]);
|
|
47
|
+
for(let bits=0;bits<8;bits++) {
|
|
48
|
+
const mods={shiftKey:!!(bits&1),altKey:!!(bits&2),ctrlKey:!!(bits&4)};
|
|
49
|
+
for(const [dx,dy,code] of [[0,-1,64],[0,1,65],[-1,0,66],[1,0,67]])
|
|
50
|
+
equal(wheel(dx,dy,mods),[code+(bits<<2)]);
|
|
51
|
+
}
|
|
52
|
+
await write('\\x1b[?1002l');equal(wheel(1,0),[]);
|
|
53
|
+
wrapper.close();return 'ok';
|
|
54
|
+
})()`});
|
|
55
|
+
assert.equal(result.exceptionDetails,undefined,JSON.stringify(result.exceptionDetails));assert.equal(result.result.value,'ok');
|
|
56
|
+
}finally{
|
|
57
|
+
ws?.close();if(child.pid){try{process.kill(-child.pid,'SIGTERM');}catch{}}
|
|
58
|
+
server.closeAllConnections();await new Promise(r=>server.close(r));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-visible changes to jsmdcui are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.19.4] - 2026-10-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Catch failures during Go regexp conversion as well as compilation, and
|
|
10
|
+
bound the converter's nested-group recursion. An excessively nested rule
|
|
11
|
+
is disabled individually instead of throwing from `SyntaxDefinition` or
|
|
12
|
+
causing the loader to silently discard the entire syntax definition.
|
|
13
|
+
Other rules remain usable; this does not limit input line length.
|
|
14
|
+
|
|
15
|
+
## [0.19.3] - 2026-10-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Restore Dockerfile highlighting and improve compatibility with Micro's
|
|
20
|
+
syntax definitions by translating Go regular-expression flags and POSIX
|
|
21
|
+
character classes into JavaScript equivalents. Scoped flags retain their
|
|
22
|
+
scope rather than changing the entire expression.
|
|
23
|
+
- Fix nested highlighting regions, especially HTML and XML attributes:
|
|
24
|
+
closing a quoted attribute now returns to the surrounding tag, and closing
|
|
25
|
+
the tag returns to normal text. Unfinished attributes still carry their
|
|
26
|
+
highlighting state across lines.
|
|
27
|
+
- Replace recursive region scanning with an iterative scanner so long,
|
|
28
|
+
minified lines can be highlighted through their final token without stack
|
|
29
|
+
exhaustion or truncation.
|
|
30
|
+
- Prevent zero-width region cycles and cyclic syntax/state trees from looping
|
|
31
|
+
indefinitely. Zero-width matches advance safely over Unicode characters,
|
|
32
|
+
and NUL characters or isolated surrogate units do not leak region state.
|
|
33
|
+
These guards do not impose a timeout on an individual JavaScript RegExp.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Add syntax samples and a reproducible comparison against Micro's Go
|
|
38
|
+
highlighter, including JavaScript, Python and Markdown with upstream syntax
|
|
39
|
+
definitions. See [the comparison guide](tests/syntax-parity/README.md) for
|
|
40
|
+
generating the HTML comparison locally; generated HTML is not bundled.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Markdown heading tests detect Bun's actual raw-HTML parsing behavior instead
|
|
45
|
+
of relying on version checks, accommodating the slash-tag behavior change
|
|
46
|
+
while preserving fail-closed coverage. The heading suites were verified on
|
|
47
|
+
Bun 1.4.0 through 1.4.4.
|
|
48
|
+
|
|
5
49
|
## [0.19.2] - 2026-09-21
|
|
6
50
|
|
|
7
51
|
### Added
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Adapt Go regexp syntax without changing the upstream syntax definitions.
|
|
2
|
+
// Scoped JS modifiers preserve flag changes across alternatives, but not out
|
|
3
|
+
// of their enclosing group. They add no capturing groups. Modifiers are only
|
|
4
|
+
// emitted where flags differ from those in effect, so flag-free patterns
|
|
5
|
+
// do not depend on engine support for them.
|
|
6
|
+
export function convertGoRegexp(pattern) {
|
|
7
|
+
const source = String(pattern);
|
|
8
|
+
if (!/\(\?[ims-]|\[\^?\]|\(\?P<|\[:[a-z]+:\]/.test(source)) return source;
|
|
9
|
+
let i = 0;
|
|
10
|
+
function expression(inherited, active, nested = false, depth = 0) {
|
|
11
|
+
// Bound syntax-rule recursion, not the length of text being highlighted.
|
|
12
|
+
if (depth > 256) throw new RangeError('Go regexp nesting exceeds 256 groups');
|
|
13
|
+
const flags = new Set(inherited);
|
|
14
|
+
let output = '', wrapped = false;
|
|
15
|
+
function open() {
|
|
16
|
+
const on = 'ims'.split('').filter(f => flags.has(f) && !active.has(f)).join('');
|
|
17
|
+
const off = 'ims'.split('').filter(f => !flags.has(f) && active.has(f)).join('');
|
|
18
|
+
if (!on && !off) return;
|
|
19
|
+
output += `(?${on}${off ? '-' + off : ''}:`;
|
|
20
|
+
wrapped = true;
|
|
21
|
+
}
|
|
22
|
+
function close() { if (wrapped) output += ')'; wrapped = false; }
|
|
23
|
+
while (i < source.length) {
|
|
24
|
+
const ch = source[i];
|
|
25
|
+
if (ch === ')' && nested) { i++; close(); return output; }
|
|
26
|
+
if (ch === '|') { close(); output += '|'; i++; continue; }
|
|
27
|
+
const directive = ch === '(' && source.slice(i).match(/^\(\?([ims]*)(?:-([ims]+))?\)/);
|
|
28
|
+
if (directive && (directive[1] || directive[2])) {
|
|
29
|
+
close();
|
|
30
|
+
for (const f of directive[1]) flags.add(f);
|
|
31
|
+
for (const f of directive[2] || '') flags.delete(f);
|
|
32
|
+
i += directive[0].length;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!wrapped) open();
|
|
36
|
+
if (ch === '\\') { output += source.slice(i, i + 2); i += 2; continue; }
|
|
37
|
+
if (ch === '[') {
|
|
38
|
+
output += source[i++];
|
|
39
|
+
if (source[i] === '^') output += source[i++];
|
|
40
|
+
// Go permits a literal ] as the first member of a character class.
|
|
41
|
+
if (source[i] === ']') { output += '\\]'; i++; }
|
|
42
|
+
while (i < source.length) {
|
|
43
|
+
const posix = source.slice(i).match(/^\[:([a-z]+):\]/);
|
|
44
|
+
if (posix && POSIX_CLASSES[posix[1]] !== undefined) {
|
|
45
|
+
output += POSIX_CLASSES[posix[1]]; i += posix[0].length; continue;
|
|
46
|
+
}
|
|
47
|
+
const c = source[i++]; output += c;
|
|
48
|
+
if (c === '\\' && i < source.length) output += source[i++];
|
|
49
|
+
else if (c === ']') break;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '(') {
|
|
54
|
+
const scoped = source.slice(i).match(/^\(\?([ims]*)(?:-([ims]+))?:/);
|
|
55
|
+
const childFlags = new Set(flags);
|
|
56
|
+
if (scoped) {
|
|
57
|
+
for (const f of scoped[1]) childFlags.add(f);
|
|
58
|
+
for (const f of scoped[2] || '') childFlags.delete(f);
|
|
59
|
+
output += '(?:'; i += scoped[0].length;
|
|
60
|
+
} else {
|
|
61
|
+
const prefix = source.slice(i).match(/^\(\?(?:[:=!]|<[=!]|<[^>]+>|P<[^>]+>)/);
|
|
62
|
+
output += prefix ? prefix[0].replace('(?P<', '(?<') : '(';
|
|
63
|
+
i += prefix ? prefix[0].length : 1;
|
|
64
|
+
}
|
|
65
|
+
output += expression(childFlags, flags, true, depth + 1) + ')';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
output += ch; i++;
|
|
69
|
+
}
|
|
70
|
+
close();
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
return expression(new Set(), new Set());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Class contents, not nested [...] expressions: e.g. [[:alnum:]._-].
|
|
77
|
+
const POSIX_CLASSES = {
|
|
78
|
+
alnum: 'A-Za-z0-9', alpha: 'A-Za-z', ascii: '\\x00-\\x7F',
|
|
79
|
+
blank: ' \\t', cntrl: '\\x00-\\x1F\\x7F', digit: '0-9',
|
|
80
|
+
graph: '!-~', lower: 'a-z', print: ' -~',
|
|
81
|
+
punct: '!-/:-@\\[-`{-~', space: '\\t\\n\\v\\f\\r ',
|
|
82
|
+
upper: 'A-Z', word: 'A-Za-z0-9_', xdigit: 'A-Fa-f0-9',
|
|
83
|
+
};
|
|
@@ -8,9 +8,7 @@ export class Highlighter {
|
|
|
8
8
|
highlightLine(line, state = null, progress = null) {
|
|
9
9
|
const text = String(line);
|
|
10
10
|
const changes = new Map([[0, state?.group ?? "default"]]);
|
|
11
|
-
const nextState = state
|
|
12
|
-
? scanRegion(changes, text, 0, state, this.rules, progress)
|
|
13
|
-
: scanTop(changes, text, 0, this.rules, progress);
|
|
11
|
+
const nextState = scanLine(changes, text, state, this.rules, progress);
|
|
14
12
|
return {
|
|
15
13
|
changes: normalizeChanges(changes, text.length),
|
|
16
14
|
state: nextState,
|
|
@@ -29,16 +27,18 @@ export class Highlighter {
|
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
export function resolveRules(rules, definitions, seen) {
|
|
30
|
+
export function resolveRules(rules, definitions, seen, ancestors = new Set()) {
|
|
31
|
+
if (ancestors.has(rules) || ancestors.size >= 128) return [];
|
|
32
|
+
const path = new Set(ancestors); path.add(rules);
|
|
33
33
|
const out = [];
|
|
34
34
|
for (const rule of rules) {
|
|
35
35
|
if (rule.type === "include") {
|
|
36
36
|
if (seen.has(rule.include)) continue;
|
|
37
37
|
seen.add(rule.include);
|
|
38
38
|
const def = definitions.find((candidate) => candidate.filetype === rule.include);
|
|
39
|
-
if (def) out.push(...resolveRules(def.rules, definitions, seen));
|
|
39
|
+
if (def) out.push(...resolveRules(def.rules, definitions, seen, path));
|
|
40
40
|
} else if (rule.type === "region") {
|
|
41
|
-
out.push({ ...rule, rules: resolveRules(rule.rules ?? [], definitions, new Set(seen)) });
|
|
41
|
+
out.push({ ...rule, rules: resolveRules(rule.rules ?? [], definitions, new Set(seen), path) });
|
|
42
42
|
} else {
|
|
43
43
|
out.push(rule);
|
|
44
44
|
}
|
|
@@ -53,67 +53,67 @@ export function flattenRules(rules, definitions, seen) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Iterative state machine: adjacent tokens on a minified line must not consume
|
|
57
|
+
// call-stack depth. A state repeated at an unchanged position steps forward.
|
|
58
|
+
function scanLine(changes, line, region, rootRules, progress) {
|
|
59
|
+
let cursor = 0, previous = -1;
|
|
60
|
+
const visited = new Set();
|
|
61
|
+
const parents = new Set();
|
|
62
|
+
for (let r = region; r; r = r.parent) {
|
|
63
|
+
if (parents.has(r) || parents.size >= 128) { region = null; changes.set(0, "default"); break; }
|
|
64
|
+
parents.add(r);
|
|
65
|
+
}
|
|
58
66
|
while (cursor <= line.length) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
if (cursor !== previous) { visited.clear(); previous = cursor; }
|
|
68
|
+
if (visited.has(region)) {
|
|
69
|
+
// A zero-width child can end and reopen forever at the same position.
|
|
70
|
+
// Skip one character in the current region, like a zero-width match.
|
|
71
|
+
cursor = advanceIndex(line, cursor);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (visited.size >= 256) {
|
|
75
|
+
// Fresh object states from malformed externally supplied trees.
|
|
76
|
+
// Discard this line's unfinished region; do not poison subsequent lines.
|
|
77
|
+
changes.set(cursor, "default"); changes.set(line.length, "default");
|
|
65
78
|
return null;
|
|
66
79
|
}
|
|
67
|
-
|
|
68
|
-
applyPatterns(changes, line, cursor, regionStart.start, rules, "default", progress);
|
|
69
|
-
changes.set(regionStart.start, regionStart.region.limitGroup ?? "default");
|
|
70
|
-
changes.set(regionStart.end, regionStart.region.group);
|
|
71
|
-
progress?.(regionStart.end);
|
|
72
|
-
|
|
73
|
-
const state = scanRegion(changes, line, regionStart.end, regionStart.region, rules, progress);
|
|
74
|
-
if (state) return state;
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function scanRegion(changes, line, pos, region, rootRules, progress = null) {
|
|
81
|
-
let cursor = pos;
|
|
82
|
-
const rules = region.rules ?? [];
|
|
83
|
-
while (cursor <= line.length) {
|
|
80
|
+
visited.add(region);
|
|
84
81
|
progress?.(cursor);
|
|
85
|
-
const
|
|
82
|
+
const rules = region?.rules ?? rootRules;
|
|
83
|
+
const end = region ? findMatch(region.end, line, cursor, region.skip, progress) : null;
|
|
86
84
|
const nested = findFirstRegion(rules, line, cursor, progress);
|
|
87
|
-
|
|
88
85
|
if (end && (!nested || end.start <= nested.start)) {
|
|
89
86
|
applyPatterns(changes, line, cursor, end.start, rules, region.group, progress);
|
|
90
87
|
changes.set(end.start, region.limitGroup ?? region.group);
|
|
91
88
|
changes.set(end.end, parentGroup(region));
|
|
92
89
|
progress?.(end.end);
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const parent = region.parent ?? null;
|
|
91
|
+
cursor = parent ? end.end : Math.max(end.end, advanceIndex(line, end.start));
|
|
92
|
+
region = parent;
|
|
93
|
+
continue;
|
|
95
94
|
}
|
|
96
|
-
|
|
97
95
|
if (nested) {
|
|
98
|
-
applyPatterns(changes, line, cursor, nested.start, rules, region
|
|
99
|
-
changes.set(nested.start, nested.region.limitGroup ?? region.group);
|
|
100
|
-
|
|
101
|
-
changes.set(nested.end,
|
|
96
|
+
applyPatterns(changes, line, cursor, nested.start, rules, region?.group ?? "default", progress);
|
|
97
|
+
changes.set(nested.start, nested.region.limitGroup ?? nested.region.group);
|
|
98
|
+
region = region ? { ...nested.region, parent: region } : nested.region;
|
|
99
|
+
changes.set(nested.end, region.group);
|
|
102
100
|
progress?.(nested.end);
|
|
103
|
-
|
|
104
|
-
if (state) return state;
|
|
105
|
-
cursor = Math.max(nested.end + 1, nextChangeAfter(changes, nested.end));
|
|
101
|
+
cursor = nested.end;
|
|
106
102
|
continue;
|
|
107
103
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
changes.set(line.length, region.group);
|
|
104
|
+
applyPatterns(changes, line, cursor, line.length, rules, region?.group ?? "default", progress);
|
|
105
|
+
changes.set(line.length, region?.group ?? "default");
|
|
111
106
|
progress?.(line.length);
|
|
112
107
|
return region;
|
|
113
108
|
}
|
|
114
109
|
return region;
|
|
115
110
|
}
|
|
116
111
|
|
|
112
|
+
// AdvanceStringIndex for Unicode regexps, including isolated surrogate units.
|
|
113
|
+
function advanceIndex(text, index) {
|
|
114
|
+
return index + (text.codePointAt(index) > 0xFFFF ? 2 : 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
117
|
function applyPatterns(changes, line, start, end, rules, fallbackGroup, progress = null) {
|
|
118
118
|
if (end < start) return;
|
|
119
119
|
const length = end - start;
|
|
@@ -161,18 +161,12 @@ function findMatch(regex, line, pos, skip, progress = null) {
|
|
|
161
161
|
const global = globalize(regex);
|
|
162
162
|
global.lastIndex = pos;
|
|
163
163
|
let match;
|
|
164
|
-
if (!progress) {
|
|
165
|
-
match = global.exec(masked);
|
|
166
|
-
if (!match) return null;
|
|
167
|
-
const start = match.index ?? 0;
|
|
168
|
-
return { start, end: start + match[0].length };
|
|
169
|
-
}
|
|
170
164
|
while ((match = global.exec(masked)) !== null) {
|
|
171
165
|
const start = match.index ?? 0;
|
|
172
166
|
const end = start + match[0].length;
|
|
173
167
|
progress?.(start);
|
|
174
168
|
if (start >= pos) return { start, end };
|
|
175
|
-
|
|
169
|
+
global.lastIndex = Math.max(global.lastIndex, advanceIndex(masked, start));
|
|
176
170
|
}
|
|
177
171
|
progress?.(line.length);
|
|
178
172
|
return null;
|
|
@@ -189,7 +183,7 @@ function findAllMatches(regex, line, start, end, progress = null) {
|
|
|
189
183
|
if (matchStart >= end) break;
|
|
190
184
|
const matchEnd = matchStart + match[0].length;
|
|
191
185
|
if (match[0].length === 0) {
|
|
192
|
-
global.lastIndex
|
|
186
|
+
global.lastIndex = advanceIndex(line, matchStart);
|
|
193
187
|
continue;
|
|
194
188
|
}
|
|
195
189
|
if (matchEnd > start) out.push({ start: Math.max(matchStart, start), end: Math.min(matchEnd, end) });
|
|
@@ -202,7 +196,7 @@ function findAllMatches(regex, line, start, end, progress = null) {
|
|
|
202
196
|
if (matchStart >= end) break;
|
|
203
197
|
progress?.(matchStart);
|
|
204
198
|
if (match[0].length === 0) {
|
|
205
|
-
global.lastIndex
|
|
199
|
+
global.lastIndex = advanceIndex(line, matchStart);
|
|
206
200
|
continue;
|
|
207
201
|
}
|
|
208
202
|
if (matchEnd <= start || matchStart >= end) continue;
|
|
@@ -229,13 +223,6 @@ function parentGroup(region) {
|
|
|
229
223
|
return region.parent?.group ?? "default";
|
|
230
224
|
}
|
|
231
225
|
|
|
232
|
-
function nextChangeAfter(changes, index) {
|
|
233
|
-
for (const key of [...changes.keys()].sort((a, b) => a - b)) {
|
|
234
|
-
if (key > index) return key;
|
|
235
|
-
}
|
|
236
|
-
return index + 1;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
226
|
function globalize(regex) {
|
|
240
227
|
const flags = new Set(regex.flags.split(""));
|
|
241
228
|
flags.add("g");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { convertGoRegexp } from "./go-regexp.js";
|
|
2
3
|
|
|
3
4
|
export class SyntaxHeader {
|
|
4
5
|
constructor({ filetype = "", filename = "", header = "", signature = "" } = {}) {
|
|
@@ -195,15 +196,17 @@ function parseRule(rule) {
|
|
|
195
196
|
|
|
196
197
|
function compileRegex(pattern) {
|
|
197
198
|
if (!pattern) return null;
|
|
198
|
-
const translated = translateGoRegexp(pattern);
|
|
199
199
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
// Conversion may reject an excessively nested rule. Keep that failure
|
|
201
|
+
// local to this regexp rather than dropping the entire syntax definition.
|
|
202
|
+
const translated = convertGoRegexp(translateGoRegexp(pattern));
|
|
202
203
|
try {
|
|
203
|
-
return new RegExp(translated);
|
|
204
|
+
return new RegExp(translated, "u");
|
|
204
205
|
} catch {
|
|
205
|
-
return
|
|
206
|
+
return new RegExp(translated);
|
|
206
207
|
}
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {convertGoRegexp} from '../src/highlight/go-regexp.js';
|
|
3
|
+
import {SyntaxDefinition,parseHeaderYaml} from '../src/highlight/parser.js';
|
|
4
|
+
const regex = s => new RegExp(convertGoRegexp(s), 'u');
|
|
5
|
+
test('Go flags apply to the remaining alternatives, preserving precedence', () => {
|
|
6
|
+
const r = regex('^a(?i)b$|^c$');
|
|
7
|
+
expect(r.test('aB')).toBe(true);
|
|
8
|
+
expect(r.test('AB')).toBe(false);
|
|
9
|
+
expect(r.test('C')).toBe(true);
|
|
10
|
+
expect(r.test('aC')).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
test('flag changes stay inside groups and can be disabled', () => {
|
|
13
|
+
const r = regex('^(a(?i)b|c)d$');
|
|
14
|
+
expect(r.test('aBd')).toBe(true);
|
|
15
|
+
expect(r.test('Cd')).toBe(true);
|
|
16
|
+
expect(r.test('CD')).toBe(false);
|
|
17
|
+
expect(regex('^(?i)a(?-i)b$').test('Ab')).toBe(true);
|
|
18
|
+
expect(regex('^(?i)a(?-i)b$').test('AB')).toBe(false);
|
|
19
|
+
expect(regex('^(?i:a(?-i:b))c$').test('Abc')).toBe(true);
|
|
20
|
+
expect(regex('^(?i:a(?-i:b))c$').test('ABC')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
test('capture numbers, scoped flags, escaped literals and class brackets survive', () => {
|
|
23
|
+
expect(regex('^(?i)(a)(b)$').exec('AB').slice(1)).toEqual(['A','B']);
|
|
24
|
+
expect(regex('(?i:a)+').test('AAA')).toBe(true);
|
|
25
|
+
expect(regex('\\(\\?i\\)').test('(?i)')).toBe(true);
|
|
26
|
+
const url = regex('(https?|ftp|ssh)://\\S*[^])>\\s,.]');
|
|
27
|
+
expect(url.exec('https://example.com)')[0]).toBe('https://example.com');
|
|
28
|
+
expect(regex('(?s)^a.b$').test('a\nb')).toBe(true);
|
|
29
|
+
expect(regex('(?m)^b$').test('a\nb\nc')).toBe(true);
|
|
30
|
+
expect(convertGoRegexp('\\bword\\b')).toBe('\\bword\\b');
|
|
31
|
+
});
|
|
32
|
+
test('all bundled syntax regexes compile, and Dockerfile keywords match', async () => {
|
|
33
|
+
const root = new URL('../runtime/syntax/', import.meta.url).pathname;
|
|
34
|
+
const failures = [];
|
|
35
|
+
function walk(rules, file) {
|
|
36
|
+
for (const r of rules) {
|
|
37
|
+
if (r.type === 'pattern' && !r.regex) failures.push(file+': '+r.source);
|
|
38
|
+
if (r.type === 'region') {
|
|
39
|
+
for (const k of ['start','end','skip']) if (r.source[k] && !r[k]) failures.push(file+': '+r.source[k]);
|
|
40
|
+
walk(r.rules,file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for await (const file of new Bun.Glob('*.yaml').scan(root)) {
|
|
45
|
+
const source = Bun.YAML.parse(await Bun.file(root+'/'+file).text());
|
|
46
|
+
const header = parseHeaderYaml(source);
|
|
47
|
+
for (const [key,re] of [['filename','fileNameRegex'],['header','headerRegex'],['signature','signatureRegex']])
|
|
48
|
+
if (header[key] && !header[re]) failures.push(file+': '+key);
|
|
49
|
+
const def = new SyntaxDefinition(header,source);
|
|
50
|
+
walk(def.rules,file);
|
|
51
|
+
if (file === 'dockerfile.yaml') {
|
|
52
|
+
expect(header.matchFileName('/tmp/Dockerfile')).toBe(true);
|
|
53
|
+
const keyword = def.rules.find(r=>r.group==='type.keyword').regex;
|
|
54
|
+
for (const line of ['FROM debian','run echo hi','COPY . /app']) expect(keyword.test(line)).toBe(true);
|
|
55
|
+
expect(keyword.test('FROMAGE debian')).toBe(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
expect(failures).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
test('mixed and negated POSIX classes retain ASCII Go semantics',()=>{
|
|
61
|
+
expect(regex('^[[:alnum:].%_+-]+@[[:alnum:].-]+$').test('a.b+tag@example.com')).toBe(true);
|
|
62
|
+
expect(regex('^[^[:space:]]+:$').test('Subject:')).toBe(true);
|
|
63
|
+
expect(regex('^[^[:space:]]+:$').test('Bad Header:')).toBe(false);
|
|
64
|
+
expect(regex('^[{[:space:]]-block([[:space:]{}]|$)').test(' -block ')).toBe(true);
|
|
65
|
+
expect(regex('^[x[:space:]]$').test('\t')).toBe(true);
|
|
66
|
+
expect(regex('^[x[:space:]]$').test('\u00a0')).toBe(false);
|
|
67
|
+
expect(regex('^[x[:punct:]]$').test('[')).toBe(true);
|
|
68
|
+
expect(regex('^[x[:punct:]]$').test('A')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
test('modifiers are emitted only where flags change',()=>{
|
|
71
|
+
expect(convertGoRegexp('[]a]')).toBe('[\\]a]');
|
|
72
|
+
expect(convertGoRegexp('(?P<x>a)')).toBe('(?<x>a)');
|
|
73
|
+
expect(convertGoRegexp('[[:alpha:]_]')).toBe('[A-Za-z_]');
|
|
74
|
+
expect(convertGoRegexp('(?i)\\b(select|from)\\b')).toBe('(?i:\\b(select|from)\\b)');
|
|
75
|
+
expect(regex('^(?i)a(?-i:b)$').test('Ab')).toBe(true);
|
|
76
|
+
expect(regex('^(?i)a(?-i:b)$').test('AB')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('converter bounds nested groups without limiting adjacent groups',()=>{
|
|
80
|
+
expect(()=>convertGoRegexp('(?i)'+'('.repeat(50000)+'x'+')'.repeat(50000))).toThrow('nesting exceeds');
|
|
81
|
+
expect(regex('(?i)'+'('.repeat(256)+'x'+')'.repeat(256)).test('X')).toBe(true);
|
|
82
|
+
expect(()=>convertGoRegexp('(?i)'+'(x)'.repeat(50000))).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('conversion failure disables only its rule and preserves the loaded syntax',async()=>{
|
|
86
|
+
const {loadSyntaxDefinitions}=await import('../src/highlight/parser.js');
|
|
87
|
+
const {Highlighter}=await import('../src/highlight/highlighter.js');
|
|
88
|
+
const deep='(?i)'+'('.repeat(50000)+'x'+')'.repeat(50000);
|
|
89
|
+
const source={filetype:'guarded',detect:{filename:'\\.guarded$'},rules:[{bad:deep},{keyword:'(?i)hello'},{invalid:'['}]};
|
|
90
|
+
const def=new SyntaxDefinition(parseHeaderYaml(source),source);
|
|
91
|
+
expect(def.rules[0].regex).toBeNull();expect(def.rules[2].regex).toBeNull();
|
|
92
|
+
expect(def.rules[1].regex.test('HELLO')).toBe(true);
|
|
93
|
+
const runtime={list:type=>type===1?[{name:'guarded',text:async()=>JSON.stringify(source)}]:[]};
|
|
94
|
+
const defs=await loadSyntaxDefinitions(runtime);
|
|
95
|
+
expect(defs.map(d=>d.filetype)).toEqual(['guarded']);
|
|
96
|
+
expect(defs[0].header.matchFileName('example.guarded')).toBe(true);
|
|
97
|
+
expect(new Highlighter(defs[0],defs).highlightLine('HELLO').changes.get(0)).toBe('keyword');
|
|
98
|
+
});
|