buninu-linux 0.15.58 → 0.17.60

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.
Files changed (228) hide show
  1. package/NOTICE.md +46 -46
  2. package/README.md +22 -15
  3. package/README.zh.md +22 -15
  4. package/initramfs/buninu/CHANGELOG.md +47 -0
  5. package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
  6. package/initramfs/buninu/apps/jsgotty/NOTICE +5 -0
  7. package/initramfs/buninu/apps/jsgotty/README.en.md +21 -0
  8. package/initramfs/buninu/apps/jsgotty/README.md +21 -0
  9. package/initramfs/buninu/apps/jsgotty/client-modes.mjs +117 -0
  10. package/initramfs/buninu/apps/jsgotty/client.mjs +100 -10
  11. package/initramfs/buninu/apps/jsgotty/clipboard-powershell.js +18 -0
  12. package/initramfs/buninu/apps/jsgotty/clipboard.js +148 -0
  13. package/initramfs/buninu/apps/jsgotty/gotty.js +13 -0
  14. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  15. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  16. package/initramfs/buninu/apps/jsgotty/static/css/index.css +14 -1
  17. package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +107 -8
  18. package/initramfs/buninu/apps/jsgotty/static/js/kitty.js +115 -7
  19. package/initramfs/buninu/apps/jsgotty/terminal-modes.js +332 -0
  20. package/initramfs/buninu/apps/jsgotty/tests/client-exit.test.mjs +45 -0
  21. package/initramfs/buninu/apps/jsgotty/tests/client-modes.test.mjs +105 -0
  22. package/initramfs/buninu/apps/jsgotty/tests/clipboard-browser.test.js +81 -0
  23. package/initramfs/buninu/apps/jsgotty/tests/clipboard.test.js +75 -0
  24. package/initramfs/buninu/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
  25. package/initramfs/buninu/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
  26. package/initramfs/buninu/apps/jsgotty/tests/manual-touch-diagnostics.js +33 -0
  27. package/initramfs/buninu/apps/jsgotty/tests/manual-touch-diagnostics.mjs +22 -0
  28. package/initramfs/buninu/apps/jsgotty/tests/nested-cli-pty.test.mjs +116 -0
  29. package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes-browser.test.js +144 -0
  30. package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes.md +44 -0
  31. package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes.test.js +166 -0
  32. package/initramfs/buninu/apps/jsgotty/tests/touch-drag-browser.test.js +106 -0
  33. package/initramfs/buninu/apps/jsgotty/tests/wheel-browser.test.js +60 -0
  34. package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +44 -0
  35. package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
  36. package/initramfs/buninu/apps/jsmdcui/src/highlight/go-regexp.js +83 -0
  37. package/initramfs/buninu/apps/jsmdcui/src/highlight/highlighter.js +49 -62
  38. package/initramfs/buninu/apps/jsmdcui/src/highlight/parser.js +8 -5
  39. package/initramfs/buninu/apps/jsmdcui/tests/go-regexp.test.js +98 -0
  40. package/initramfs/buninu/apps/jsmdcui/tests/heading-ids.test.js +21 -16
  41. package/initramfs/buninu/apps/jsmdcui/tests/highlight-regions.test.js +73 -0
  42. package/initramfs/buninu/apps/jsmdcui/tests/id-collision.test.js +32 -9
  43. package/initramfs/buninu/apps/jsmdcui/tests/markdown-behavior.mjs +9 -0
  44. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/NORMAL-HTML.md +46 -0
  45. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/README.md +85 -0
  46. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/REPORT.md +328 -0
  47. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/compare.mjs +75 -0
  48. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/go.mod +5 -0
  49. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/normal-html-report.json +619 -0
  50. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/normal-html.html +9 -0
  51. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/oracle.go +49 -0
  52. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/report.json +2174 -0
  53. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/PowerShell.txt +39 -0
  54. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ada.txt +25 -0
  55. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/apacheconf.txt +49 -0
  56. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/arduino.txt +38 -0
  57. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/asciidoc.txt +33 -0
  58. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/asm.txt +77 -0
  59. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ats.txt +48 -0
  60. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/awk.txt +28 -0
  61. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/b.txt +27 -0
  62. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/bat.txt +39 -0
  63. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/c.txt +34 -0
  64. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/caddyfile.txt +14 -0
  65. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cake.txt +7 -0
  66. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/clojure.txt +18 -0
  67. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cmake.txt +20 -0
  68. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/coffeescript.txt +32 -0
  69. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/colortest.txt +19 -0
  70. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/conky.txt +12 -0
  71. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cpp.txt +46 -0
  72. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/crontab.txt +16 -0
  73. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/crystal.txt +28 -0
  74. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/csharp.txt +28 -0
  75. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/css.txt +19 -0
  76. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/csx.txt +7 -0
  77. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cuda.txt +37 -0
  78. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cython.txt +22 -0
  79. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/d.txt +52 -0
  80. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dart.txt +27 -0
  81. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/default.txt +8 -0
  82. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dockerfile.txt +10 -0
  83. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dot.txt +15 -0
  84. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/elixir.txt +23 -0
  85. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/elm.txt +20 -0
  86. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/erb.txt +32 -0
  87. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/erlang.txt +23 -0
  88. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fish.txt +20 -0
  89. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/forth.txt +17 -0
  90. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fortran.txt +40 -0
  91. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/freebsd-kernel.txt +10 -0
  92. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fsharp.txt +27 -0
  93. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gdscript.txt +26 -0
  94. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gemini.txt +11 -0
  95. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gentoo-ebuild.txt +28 -0
  96. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gentoo-etc-portage.txt +14 -0
  97. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-commit.txt +19 -0
  98. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-config.txt +11 -0
  99. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-rebase-todo.txt +10 -0
  100. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gleam.txt +44 -0
  101. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/glsl.txt +18 -0
  102. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gnuplot.txt +11 -0
  103. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/go.txt +33 -0
  104. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/godoc.txt +9 -0
  105. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/golo.txt +50 -0
  106. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gomod.txt +16 -0
  107. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/graphql.txt +20 -0
  108. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/groff.txt +18 -0
  109. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/groovy.txt +42 -0
  110. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/haml.txt +16 -0
  111. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/hare.txt +31 -0
  112. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/haskell.txt +26 -0
  113. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/hc.txt +28 -0
  114. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html.txt +26 -0
  115. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html4.txt +20 -0
  116. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html5.txt +20 -0
  117. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ini.txt +16 -0
  118. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/inputrc.txt +14 -0
  119. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/java.txt +18 -0
  120. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/javascript.txt +41 -0
  121. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/jinja2.txt +13 -0
  122. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/json.txt +21 -0
  123. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/jsonnet.txt +28 -0
  124. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/julia.txt +24 -0
  125. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/justfile.txt +26 -0
  126. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/keymap.txt +15 -0
  127. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kickstart.txt +16 -0
  128. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kotlin.txt +30 -0
  129. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kvlang.txt +37 -0
  130. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ledger.txt +13 -0
  131. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lfe.txt +17 -0
  132. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lilypond.txt +15 -0
  133. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lisp.txt +17 -0
  134. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/log.txt +45 -0
  135. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lua.txt +48 -0
  136. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mail.txt +17 -0
  137. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/makefile.txt +25 -0
  138. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/man.txt +12 -0
  139. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/markdown.txt +21 -0
  140. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mc.txt +11 -0
  141. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/meson.txt +29 -0
  142. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/micro.txt +25 -0
  143. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mpdconf.txt +13 -0
  144. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/msbuild.txt +7 -0
  145. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nanorc.txt +16 -0
  146. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nftables.txt +22 -0
  147. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nginx.txt +17 -0
  148. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nim.txt +23 -0
  149. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nix.txt +15 -0
  150. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nu.txt +92 -0
  151. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/objc.txt +36 -0
  152. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ocaml.txt +25 -0
  153. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/octave.txt +35 -0
  154. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/odin.txt +35 -0
  155. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pascal.txt +22 -0
  156. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/patch.txt +13 -0
  157. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/peg.txt +16 -0
  158. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/perl.txt +27 -0
  159. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/php.txt +44 -0
  160. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pkg-config.txt +12 -0
  161. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/po.txt +12 -0
  162. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pony.txt +29 -0
  163. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pov.txt +17 -0
  164. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-action.txt +14 -0
  165. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-config.txt +10 -0
  166. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-filter.txt +12 -0
  167. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/proto.txt +22 -0
  168. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/prql.txt +42 -0
  169. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/puppet.txt +22 -0
  170. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/python2.txt +24 -0
  171. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/python3.txt +28 -0
  172. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/r.txt +16 -0
  173. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/raku.txt +19 -0
  174. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/reST.txt +18 -0
  175. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/renpy.txt +11 -0
  176. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/rpmspec.txt +31 -0
  177. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ruby.txt +34 -0
  178. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/rust.txt +27 -0
  179. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sage.txt +24 -0
  180. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/scad.txt +23 -0
  181. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/scala.txt +16 -0
  182. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sed.txt +12 -0
  183. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sh.txt +20 -0
  184. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sls.txt +15 -0
  185. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/smalltalk.txt +28 -0
  186. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/solidity.txt +26 -0
  187. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sql.txt +35 -0
  188. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/stata.txt +32 -0
  189. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/svelte.txt +10 -0
  190. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/swift.txt +53 -0
  191. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/systemd.txt +15 -0
  192. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/tcl.txt +17 -0
  193. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/terraform.txt +28 -0
  194. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/tex.txt +15 -0
  195. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/toml.txt +31 -0
  196. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/twig.txt +29 -0
  197. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/typescript.txt +32 -0
  198. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/v.txt +47 -0
  199. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vala.txt +22 -0
  200. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/verilog.txt +37 -0
  201. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vhdl.txt +37 -0
  202. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vi.txt +17 -0
  203. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vue.txt +29 -0
  204. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/xml.txt +14 -0
  205. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/xresources.txt +14 -0
  206. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/yaml.txt +19 -0
  207. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/yum.txt +12 -0
  208. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zig.txt +24 -0
  209. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zscript.txt +31 -0
  210. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zsh.txt +21 -0
  211. package/initramfs/buninu/package.json +1 -1
  212. package/initramfs/lib/bunterm/images.js +5 -1
  213. package/initramfs/lib/bunterm/raw-protocol.js +82 -4
  214. package/initramfs/lib/bunterm/renderer.js +35 -16
  215. package/initramfs/lib/bunterm/terminal.js +11 -1
  216. package/initramfs/lib/fbdev.js +11 -5
  217. package/initramfs/lib/mapped-frame.js +28 -0
  218. package/initramfs/lib/raw-image.js +29 -5
  219. package/package.json +1 -1
  220. package/scripts/arch.js +5 -5
  221. package/scripts/arch.sh +5 -5
  222. package/scripts/npm-packages.md +11 -10
  223. package/test/bunterm-image-bytes.test.js +15 -0
  224. package/test/bunterm-mapped-lifetime.test.js +147 -0
  225. package/test/bunterm-raw-protocol.test.js +20 -0
  226. package/test/bunterm-renderer.test.js +9 -0
  227. package/test/fbdev-sync.test.js +9 -0
  228. package/test/fetch-npm.test.js +2 -2
@@ -0,0 +1,166 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const vm = require('node:vm');
6
+ const { TerminalModeTracker, modeTransition } = require('../terminal-modes.js');
7
+
8
+ const source = fs.readFileSync(path.join(__dirname, '../static/js/gotty.js'), 'utf8');
9
+ const methods = source.slice(source.indexOf(' enqueueTerminal(task)'), source.indexOf(' setDisconnected(e)'));
10
+
11
+ function feed(tracker, ...chunks) {
12
+ for (const chunk of chunks) tracker.consume(Buffer.from(chunk, 'latin1'));
13
+ return tracker.snapshot();
14
+ }
15
+
16
+ function client({ alternate = false, dec = {}, mouse = 'NONE', encoding = 'DEFAULT', cursorHidden = false } = {}) {
17
+ const Wrapper = vm.runInNewContext(`(class {${methods}})`);
18
+ const w = new Wrapper();
19
+ const writes = [];
20
+ const defaults = { applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false,
21
+ origin: false, reverseWraparound: false, sendFocus: false, wraparound: true };
22
+ w.term = {
23
+ _core: { coreService: { decPrivateModes: { ...defaults, ...dec }, isCursorHidden: cursorHidden },
24
+ coreMouseService: { activeProtocol: mouse, activeEncoding: encoding } },
25
+ buffer: { active: { type: alternate ? 'alternate' : 'normal' } },
26
+ write: (s, callback) => { if (s) writes.push(s); callback?.(); },
27
+ };
28
+ w.kittyKeyboard = { states: { normal: { flags: 0, stack: [] }, alternate: { flags: 0, stack: [] } } };
29
+ return { w, writes };
30
+ }
31
+
32
+ test('tracker follows DEC, mouse, alternate screen and kitty keyboard modes', () => {
33
+ const state = feed(new TerminalModeTracker(),
34
+ 'hi\x1b[?1049h\x1b[?1;2004h\x1b=\x1b[?25l',
35
+ '\x1b[?1002h\x1b[?10', '06h', // split across chunks
36
+ '\x1b[>1u\x1b[>11u\x1b]0;title \x1b[?2004l\x07');
37
+ assert.equal(state.alternate, true);
38
+ assert.equal(state.dec.applicationCursorKeys, true);
39
+ assert.equal(state.dec.bracketedPasteMode, true, 'OSC payload is not parsed as CSI');
40
+ assert.equal(state.dec.applicationKeypad, true);
41
+ assert.equal(state.cursorHidden, true);
42
+ assert.equal(state.mouse, 'DRAG');
43
+ assert.equal(state.encoding, 'SGR');
44
+ assert.deepEqual(state.kitty.alternate, { flags: 11, stack: [0, 1] });
45
+ assert.deepEqual(state.kitty.normal, { flags: 0, stack: [] });
46
+ });
47
+
48
+ test('tracker handles mode resets, kitty pop/set and RIS', () => {
49
+ const tracker = new TerminalModeTracker();
50
+ let state = feed(tracker, '\x1b[?1003h\x1b[?1006h\x1b[?1000l\x1b[?1006l\x1b[>1u\x1b[=8;2u\x1b[<u');
51
+ assert.equal(state.mouse, 'NONE');
52
+ assert.equal(state.encoding, 'DEFAULT');
53
+ assert.equal(state.kitty.normal.flags, 0);
54
+ state = feed(tracker, '\x1b[?1049h\x1b[?2004h\x1b[?1002h\x1b[!p');
55
+ assert.equal(state.dec.bracketedPasteMode, false, 'DECSTR resets DEC modes');
56
+ assert.equal(state.alternate, true, 'DECSTR keeps the active screen');
57
+ assert.equal(state.mouse, 'DRAG', 'DECSTR keeps mouse reporting');
58
+ state = feed(tracker, '\x1bc');
59
+ assert.deepEqual(state, new TerminalModeTracker().snapshot());
60
+ });
61
+
62
+ test('fresh client gets the full state restored', () => {
63
+ const state = feed(new TerminalModeTracker(), '\x1b[?1049h\x1b[?2004h\x1b[?1002h\x1b[?1006h\x1b[?25l\x1b[>11u');
64
+ const { w, writes } = client();
65
+ w.applySessionModes(state);
66
+ assert.equal(writes.length, 1);
67
+ for (const seq of ['\x1b[?1049h', '\x1b[?2004h', '\x1b[?1002h', '\x1b[?1006h', '\x1b[?25l'])
68
+ assert.ok(writes[0].includes(seq), JSON.stringify(seq));
69
+ assert.equal(w.kittyKeyboard.states.alternate.flags, 11);
70
+ });
71
+
72
+ test('browser masks kitty flags it does not implement', () => {
73
+ const { w } = client();
74
+ w.applySessionModes(feed(new TerminalModeTracker(), '\x1b[>31u\x1b[>31u'));
75
+ assert.equal(JSON.stringify(w.kittyKeyboard.states.normal), JSON.stringify({ flags: 11, stack: [0, 11] }));
76
+ });
77
+
78
+ test('socket-only reconnect with matching xterm state writes nothing', () => {
79
+ const state = feed(new TerminalModeTracker(), '\x1b[?1049h\x1b[?6h\x1b[?2004h\x1b[?1002h\x1b[?1006h\x1b[>1u\x1b[>11u');
80
+ const { w, writes } = client({ alternate: true, dec: { origin: true, bracketedPasteMode: true }, mouse: 'DRAG', encoding: 'SGR' });
81
+ w.applySessionModes(state);
82
+ assert.equal(writes.length, 0, 'no DECOM cursor homing or 1049 cursor save');
83
+ w.applySessionModes(state);
84
+ assert.deepEqual(JSON.parse(JSON.stringify(w.kittyKeyboard.states.alternate)), { flags: 11, stack: [0, 1] }, 'push stack does not grow');
85
+ });
86
+
87
+ test('stale client modes are turned off when the program disabled them while disconnected', () => {
88
+ const state = new TerminalModeTracker().snapshot();
89
+ const { w, writes } = client({ alternate: true, dec: { bracketedPasteMode: true }, mouse: 'ANY', encoding: 'SGR' });
90
+ w.applySessionModes(state);
91
+ assert.equal(writes.length, 1);
92
+ for (const seq of ['\x1b[?1049l', '\x1b[?2004l', '\x1b[?1000l', '\x1b[?1006l'])
93
+ assert.ok(writes[0].includes(seq), JSON.stringify(seq));
94
+ });
95
+
96
+ test('transport disconnect and reconnect clear held keys without resetting negotiation', () => {
97
+ const start = source.indexOf(' reset() {\n // Reconnecting');
98
+ const reset = source.slice(start, source.indexOf(' close() {', start));
99
+ assert.match(reset, /clearHeld\(false\)/);
100
+ assert.doesNotMatch(reset, /kittyKeyboard.reset/);
101
+ assert.match(source, /setDisconnected\(e\) \{\s*if \(e\) this.kittyKeyboard.clearHeld\(false\)/);
102
+ });
103
+
104
+ const sessions = {
105
+ vim: '\x1b[?1049h\x1b[?1h\x1b=\x1b[?2004h\x1b[?1002h\x1b[?1006h\x1b[?25l',
106
+ kittyBothScreens: '\x1b[>1u\x1b[?1049h\x1b[>31u\x1b[>3u\x1b[?1003h\x1b[?1016h',
107
+ kittySetThenPush: '\x1b[=5;1u\x1b[>8u\x1b[?1004h\x1b[?6h',
108
+ };
109
+
110
+ for (const [name, output] of Object.entries(sessions)) {
111
+ test(`CLI resume and cleanup round-trip: ${name}`, () => {
112
+ const target = feed(new TerminalModeTracker(), output);
113
+ const local = new TerminalModeTracker();
114
+ assert.deepEqual(feed(local, modeTransition(local.snapshot(), target)), target, 'fresh terminal reaches PTY state');
115
+ assert.equal(modeTransition(local.snapshot(), target), '', 'nothing left to send once in sync');
116
+ assert.deepEqual(feed(local, modeTransition(local.snapshot())), new TerminalModeTracker().snapshot(), 'cleanup returns to defaults');
117
+ });
118
+ }
119
+
120
+ test('kitty flags are kept unmasked for terminals that support all of them', () => {
121
+ assert.equal(feed(new TerminalModeTracker(), '\x1b[>31u').kitty.normal.flags, 31);
122
+ });
123
+
124
+ test('incremental Kitty restoration preserves a non-default parent stack', () => {
125
+ const local = new TerminalModeTracker();
126
+ feed(local, '\x1b[=1u\x1b[>11u');
127
+ const parent = local.snapshot();
128
+ const remote = feed(new TerminalModeTracker(), '\x1b[>3u\x1b[>9u');
129
+ const attach = modeTransition(parent, remote);
130
+ assert.doesNotMatch(attach, /<100u/);
131
+ assert.deepEqual(feed(local, attach), remote);
132
+ assert.deepEqual(feed(local, modeTransition(local.snapshot(), parent)), parent);
133
+ });
134
+
135
+ test('unknown original modes are not replaced with guessed defaults on cleanup', () => {
136
+ const unknown = new TerminalModeTracker().snapshot();
137
+ for (const key of Object.keys(unknown.dec)) unknown.dec[key] = null;
138
+ for (const key of ['alternate','cursorHidden','mouse','encoding']) unknown[key] = null;
139
+ unknown.kitty.normal.flags = unknown.kitty.alternate.flags = null;
140
+ assert.equal(modeTransition(feed(new TerminalModeTracker(), '\x1b[?1049h\x1b[>11u'), unknown), '');
141
+ });
142
+
143
+ test('hidden Kitty restoration is negotiated and never toggles the screen',()=>{
144
+ for(const alternate of [false,true]){
145
+ const local=new TerminalModeTracker();if(alternate)feed(local,'\x1b[?1049h');
146
+ const target=local.snapshot(),hidden=alternate?'normal':'alternate';
147
+ target.kitty[hidden]={flags:9,stack:[0,1]};
148
+ assert.equal(modeTransition(local.snapshot(),target),'');
149
+ const sequence=modeTransition(local.snapshot(),target,{kittyRestore:true});
150
+ assert.doesNotMatch(sequence,/\[\?(?:47|1047|1049)[hl]/);
151
+ // Every-byte fragmentation must be handled by intermediate server trackers.
152
+ for(const byte of Buffer.from(sequence))local.consume(Buffer.from([byte]));
153
+ assert.deepEqual(local.snapshot(),target);
154
+ }
155
+ });
156
+ test('Kitty restore tracker rejects malformed or unrelated string commands',()=>{
157
+ const t=new TerminalModeTracker(),before=t.snapshot();
158
+ for(const payload of [
159
+ '\x1b]777;jsgotty-kitty-v1;normal;32;0\x07',
160
+ '\x1b]777;jsgotty-kitty-v1;normal;1;'+Array(33).fill('0').join(',')+'\x07',
161
+ '\x1bP777;jsgotty-kitty-v1;normal;1;0\x1b\\',
162
+ '\x1b]777;jsgotty-kitty-v1;normal;1;0\x18',
163
+ ]){feed(t,payload);assert.deepEqual(t.snapshot(),before);}
164
+ feed(t,'\x1b]777;jsgotty-kitty-v1;normal;1;0\x1b\\');
165
+ assert.deepEqual(t.kitty.normal,{flags:1,stack:[0]});
166
+ });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsmdcui",
3
- "version": "0.19.2",
3
+ "version": "0.19.4",
4
4
  "description": "Markdown as a Common UI for Terminals and Web Browsers",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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
+ };