buninu 0.1.1

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 (435) hide show
  1. package/.bashrc +11 -0
  2. package/ARCHITECTURE.md +452 -0
  3. package/LICENSE +21 -0
  4. package/README.md +121 -0
  5. package/apps/bun/make-bunBin.sh +84 -0
  6. package/apps/jsgotty/LICENSE +23 -0
  7. package/apps/jsgotty/LICENSES/Apache-2.0-zmodem.js.txt +201 -0
  8. package/apps/jsgotty/NOTICE +21 -0
  9. package/apps/jsgotty/README.en.md +290 -0
  10. package/apps/jsgotty/README.md +292 -0
  11. package/apps/jsgotty/client.mjs +700 -0
  12. package/apps/jsgotty/env.sh +2 -0
  13. package/apps/jsgotty/gotty.js +3325 -0
  14. package/apps/jsgotty/node_modules/.package-lock.json +35 -0
  15. package/apps/jsgotty/node_modules/node-addon-api/LICENSE.md +9 -0
  16. package/apps/jsgotty/node_modules/node-addon-api/README.md +319 -0
  17. package/apps/jsgotty/node_modules/node-addon-api/common.gypi +20 -0
  18. package/apps/jsgotty/node_modules/node-addon-api/except.gypi +25 -0
  19. package/apps/jsgotty/node_modules/node-addon-api/index.js +12 -0
  20. package/apps/jsgotty/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  21. package/apps/jsgotty/node_modules/node-addon-api/napi-inl.h +6607 -0
  22. package/apps/jsgotty/node_modules/node-addon-api/napi.h +3201 -0
  23. package/apps/jsgotty/node_modules/node-addon-api/node_addon_api.gyp +32 -0
  24. package/apps/jsgotty/node_modules/node-addon-api/node_api.gyp +9 -0
  25. package/apps/jsgotty/node_modules/node-addon-api/noexcept.gypi +26 -0
  26. package/apps/jsgotty/node_modules/node-addon-api/nothing.c +0 -0
  27. package/apps/jsgotty/node_modules/node-addon-api/package-support.json +21 -0
  28. package/apps/jsgotty/node_modules/node-addon-api/package.json +480 -0
  29. package/apps/jsgotty/node_modules/node-addon-api/tools/README.md +73 -0
  30. package/apps/jsgotty/node_modules/node-addon-api/tools/check-napi.js +99 -0
  31. package/apps/jsgotty/node_modules/node-addon-api/tools/clang-format.js +71 -0
  32. package/apps/jsgotty/node_modules/node-addon-api/tools/conversion.js +301 -0
  33. package/apps/jsgotty/node_modules/node-addon-api/tools/eslint-format.js +79 -0
  34. package/apps/jsgotty/node_modules/ws/LICENSE +20 -0
  35. package/apps/jsgotty/node_modules/ws/README.md +548 -0
  36. package/apps/jsgotty/node_modules/ws/browser.js +8 -0
  37. package/apps/jsgotty/node_modules/ws/index.js +22 -0
  38. package/apps/jsgotty/node_modules/ws/lib/buffer-util.js +131 -0
  39. package/apps/jsgotty/node_modules/ws/lib/constants.js +19 -0
  40. package/apps/jsgotty/node_modules/ws/lib/event-target.js +292 -0
  41. package/apps/jsgotty/node_modules/ws/lib/extension.js +203 -0
  42. package/apps/jsgotty/node_modules/ws/lib/limiter.js +55 -0
  43. package/apps/jsgotty/node_modules/ws/lib/permessage-deflate.js +528 -0
  44. package/apps/jsgotty/node_modules/ws/lib/receiver.js +760 -0
  45. package/apps/jsgotty/node_modules/ws/lib/sender.js +607 -0
  46. package/apps/jsgotty/node_modules/ws/lib/stream.js +161 -0
  47. package/apps/jsgotty/node_modules/ws/lib/subprotocol.js +62 -0
  48. package/apps/jsgotty/node_modules/ws/lib/validation.js +152 -0
  49. package/apps/jsgotty/node_modules/ws/lib/websocket-server.js +562 -0
  50. package/apps/jsgotty/node_modules/ws/lib/websocket.js +1407 -0
  51. package/apps/jsgotty/node_modules/ws/package.json +70 -0
  52. package/apps/jsgotty/node_modules/ws/wrapper.mjs +21 -0
  53. package/apps/jsgotty/package-lock.json +46 -0
  54. package/apps/jsgotty/package.json +13 -0
  55. package/apps/jsgotty/patches/windowsPtyAgent.js +326 -0
  56. package/apps/jsgotty/patches/windowsTerminal.js +204 -0
  57. package/apps/jsgotty/rz.js +199 -0
  58. package/apps/jsgotty/single-exe/README.md +80 -0
  59. package/apps/jsgotty/single-exe/assetsHelper.js +90 -0
  60. package/apps/jsgotty/single-exe/assetsLoader.mjs +85 -0
  61. package/apps/jsgotty/single-exe/compiled.js +138 -0
  62. package/apps/jsgotty/single-exe/entry.mjs +9 -0
  63. package/apps/jsgotty/single-exe/packAssets.sh +7 -0
  64. package/apps/jsgotty/static/css/index.css +13 -0
  65. package/apps/jsgotty/static/css/kitty.css +61 -0
  66. package/apps/jsgotty/static/css/xterm.css +218 -0
  67. package/apps/jsgotty/static/css/xterm_customize.css +32 -0
  68. package/apps/jsgotty/static/favicon.ico +0 -0
  69. package/apps/jsgotty/static/help.md +125 -0
  70. package/apps/jsgotty/static/icon.svg +66 -0
  71. package/apps/jsgotty/static/icon_192.png +0 -0
  72. package/apps/jsgotty/static/index.html +164 -0
  73. package/apps/jsgotty/static/js/gotty.js +31537 -0
  74. package/apps/jsgotty/static/js/gotty.js.LICENSE.txt +7 -0
  75. package/apps/jsgotty/static/js/gotty.js.map +1 -0
  76. package/apps/jsgotty/static/js/gotty.licenses.txt +650 -0
  77. package/apps/jsgotty/static/js/kitty.js +1045 -0
  78. package/apps/jsgotty/static/js/windows_bridge.js +194 -0
  79. package/apps/jsgotty/static/js/xterm-addon-unicode11.js +2 -0
  80. package/apps/jsgotty/static/js/zmodem.js +1 -0
  81. package/apps/jsgotty/static/manifest.json +15 -0
  82. package/apps/jsgotty/sz.js +273 -0
  83. package/apps/jsgotty/tests/kitty-order.test.js +58 -0
  84. package/apps/jsgotty/viu.mjs +393 -0
  85. package/apps/jsgotty/wgotty +62 -0
  86. package/apps/jsgotty/windows-bridge-node.js +60 -0
  87. package/apps/jsgotty/zmodem-node.js +260 -0
  88. package/apps/jsmdcui/CHANGELOG.md +1048 -0
  89. package/apps/jsmdcui/LICENSE +22 -0
  90. package/apps/jsmdcui/README.md +1269 -0
  91. package/apps/jsmdcui/cdp-maze.js +141 -0
  92. package/apps/jsmdcui/clean.sh +9 -0
  93. package/apps/jsmdcui/demos/basic.jpg +0 -0
  94. package/apps/jsmdcui/demos/chinese.md +32 -0
  95. package/apps/jsmdcui/demos/event.md +170 -0
  96. package/apps/jsmdcui/demos/good.jpg +0 -0
  97. package/apps/jsmdcui/demos/image-processor.md +366 -0
  98. package/apps/jsmdcui/demos/image-processor.zh-TW.md +366 -0
  99. package/apps/jsmdcui/demos/img-change.md +73 -0
  100. package/apps/jsmdcui/demos/imgtool-zh.md +298 -0
  101. package/apps/jsmdcui/demos/imgtool.md +298 -0
  102. package/apps/jsmdcui/demos/maze.md +149 -0
  103. package/apps/jsmdcui/demos/reactive.md +40 -0
  104. package/apps/jsmdcui/demos/reader.md +513 -0
  105. package/apps/jsmdcui/demos/select.md +63 -0
  106. package/apps/jsmdcui/demos/todo-zh.md +190 -0
  107. package/apps/jsmdcui/demos/todo.md +191 -0
  108. package/apps/jsmdcui/edit +9 -0
  109. package/apps/jsmdcui/jsmdcui.sh +5 -0
  110. package/apps/jsmdcui/llm-maze.txt +94 -0
  111. package/apps/jsmdcui/package.json +19 -0
  112. package/apps/jsmdcui/project_structure.txt +35 -0
  113. package/apps/jsmdcui/runmd.mjs +1140 -0
  114. package/apps/jsmdcui/runtime/colorschemes/atom-dark.micro +33 -0
  115. package/apps/jsmdcui/runtime/colorschemes/bubblegum.micro +31 -0
  116. package/apps/jsmdcui/runtime/colorschemes/cmc-16.micro +47 -0
  117. package/apps/jsmdcui/runtime/colorschemes/cmc-tc.micro +43 -0
  118. package/apps/jsmdcui/runtime/colorschemes/darcula.micro +34 -0
  119. package/apps/jsmdcui/runtime/colorschemes/default.micro +1 -0
  120. package/apps/jsmdcui/runtime/colorschemes/dracula-tc.micro +49 -0
  121. package/apps/jsmdcui/runtime/colorschemes/dukedark-tc.micro +38 -0
  122. package/apps/jsmdcui/runtime/colorschemes/dukelight-tc.micro +38 -0
  123. package/apps/jsmdcui/runtime/colorschemes/dukeubuntu-tc.micro +38 -0
  124. package/apps/jsmdcui/runtime/colorschemes/geany.micro +29 -0
  125. package/apps/jsmdcui/runtime/colorschemes/gotham.micro +29 -0
  126. package/apps/jsmdcui/runtime/colorschemes/gruvbox-tc.micro +29 -0
  127. package/apps/jsmdcui/runtime/colorschemes/gruvbox.micro +26 -0
  128. package/apps/jsmdcui/runtime/colorschemes/material-tc.micro +36 -0
  129. package/apps/jsmdcui/runtime/colorschemes/monokai-dark.micro +28 -0
  130. package/apps/jsmdcui/runtime/colorschemes/monokai.micro +34 -0
  131. package/apps/jsmdcui/runtime/colorschemes/one-dark.micro +39 -0
  132. package/apps/jsmdcui/runtime/colorschemes/railscast.micro +37 -0
  133. package/apps/jsmdcui/runtime/colorschemes/simple.micro +33 -0
  134. package/apps/jsmdcui/runtime/colorschemes/solarized-tc.micro +31 -0
  135. package/apps/jsmdcui/runtime/colorschemes/solarized.micro +30 -0
  136. package/apps/jsmdcui/runtime/colorschemes/sunny-day.micro +29 -0
  137. package/apps/jsmdcui/runtime/colorschemes/twilight.micro +40 -0
  138. package/apps/jsmdcui/runtime/colorschemes/zenburn.micro +30 -0
  139. package/apps/jsmdcui/runtime/help/actions.md +161 -0
  140. package/apps/jsmdcui/runtime/help/cdp.md +124 -0
  141. package/apps/jsmdcui/runtime/help/colors.md +421 -0
  142. package/apps/jsmdcui/runtime/help/commands.md +161 -0
  143. package/apps/jsmdcui/runtime/help/copypaste.md +149 -0
  144. package/apps/jsmdcui/runtime/help/defaultkeys.md +148 -0
  145. package/apps/jsmdcui/runtime/help/help.md +1269 -0
  146. package/apps/jsmdcui/runtime/help/keybindings.md +355 -0
  147. package/apps/jsmdcui/runtime/help/linter.md +90 -0
  148. package/apps/jsmdcui/runtime/help/options.md +709 -0
  149. package/apps/jsmdcui/runtime/help/plugins.md +544 -0
  150. package/apps/jsmdcui/runtime/help/tutorial.md +112 -0
  151. package/apps/jsmdcui/runtime/jsplugins/cdp/cdp-server.js +1185 -0
  152. package/apps/jsmdcui/runtime/jsplugins/cdp/cdp.js +201 -0
  153. package/apps/jsmdcui/runtime/jsplugins/chapter/chapter.js +111 -0
  154. package/apps/jsmdcui/runtime/jsplugins/diff/diff.js +46 -0
  155. package/apps/jsmdcui/runtime/jsplugins/example/example.js +117 -0
  156. package/apps/jsmdcui/runtime/jsplugins/linter/linter.js +281 -0
  157. package/apps/jsmdcui/runtime/plugins/autoclose/autoclose.lua +75 -0
  158. package/apps/jsmdcui/runtime/plugins/ftoptions/ftoptions.lua +17 -0
  159. package/apps/jsmdcui/runtime/plugins/literate/README.md +5 -0
  160. package/apps/jsmdcui/runtime/plugins/literate/literate.lua +55 -0
  161. package/apps/jsmdcui/runtime/plugins/status/help/status.md +21 -0
  162. package/apps/jsmdcui/runtime/plugins/status/status.lua +62 -0
  163. package/apps/jsmdcui/runtime/syntax/LICENSE +22 -0
  164. package/apps/jsmdcui/runtime/syntax/PowerShell.yaml +128 -0
  165. package/apps/jsmdcui/runtime/syntax/README.md +63 -0
  166. package/apps/jsmdcui/runtime/syntax/ada.yaml +43 -0
  167. package/apps/jsmdcui/runtime/syntax/apacheconf.yaml +59 -0
  168. package/apps/jsmdcui/runtime/syntax/arduino.yaml +101 -0
  169. package/apps/jsmdcui/runtime/syntax/asciidoc.yaml +51 -0
  170. package/apps/jsmdcui/runtime/syntax/asm.yaml +123 -0
  171. package/apps/jsmdcui/runtime/syntax/ats.yaml +99 -0
  172. package/apps/jsmdcui/runtime/syntax/awk.yaml +44 -0
  173. package/apps/jsmdcui/runtime/syntax/b.yaml +87 -0
  174. package/apps/jsmdcui/runtime/syntax/bat.yaml +57 -0
  175. package/apps/jsmdcui/runtime/syntax/c.yaml +60 -0
  176. package/apps/jsmdcui/runtime/syntax/caddyfile.yaml +23 -0
  177. package/apps/jsmdcui/runtime/syntax/cake.yaml +7 -0
  178. package/apps/jsmdcui/runtime/syntax/clojure.yaml +38 -0
  179. package/apps/jsmdcui/runtime/syntax/cmake.yaml +42 -0
  180. package/apps/jsmdcui/runtime/syntax/coffeescript.yaml +56 -0
  181. package/apps/jsmdcui/runtime/syntax/colortest.yaml +19 -0
  182. package/apps/jsmdcui/runtime/syntax/conky.yaml +17 -0
  183. package/apps/jsmdcui/runtime/syntax/cpp.yaml +91 -0
  184. package/apps/jsmdcui/runtime/syntax/crontab.yaml +36 -0
  185. package/apps/jsmdcui/runtime/syntax/crystal.yaml +72 -0
  186. package/apps/jsmdcui/runtime/syntax/csharp.yaml +52 -0
  187. package/apps/jsmdcui/runtime/syntax/css.yaml +44 -0
  188. package/apps/jsmdcui/runtime/syntax/csx.yaml +8 -0
  189. package/apps/jsmdcui/runtime/syntax/cuda.yaml +68 -0
  190. package/apps/jsmdcui/runtime/syntax/cython.yaml +52 -0
  191. package/apps/jsmdcui/runtime/syntax/d.yaml +121 -0
  192. package/apps/jsmdcui/runtime/syntax/dart.yaml +46 -0
  193. package/apps/jsmdcui/runtime/syntax/default.yaml +10 -0
  194. package/apps/jsmdcui/runtime/syntax/dockerfile.yaml +36 -0
  195. package/apps/jsmdcui/runtime/syntax/dot.yaml +29 -0
  196. package/apps/jsmdcui/runtime/syntax/elixir.yaml +30 -0
  197. package/apps/jsmdcui/runtime/syntax/elm.yaml +38 -0
  198. package/apps/jsmdcui/runtime/syntax/erb.yaml +42 -0
  199. package/apps/jsmdcui/runtime/syntax/erlang.yaml +45 -0
  200. package/apps/jsmdcui/runtime/syntax/fish.yaml +48 -0
  201. package/apps/jsmdcui/runtime/syntax/forth.yaml +34 -0
  202. package/apps/jsmdcui/runtime/syntax/fortran.yaml +64 -0
  203. package/apps/jsmdcui/runtime/syntax/freebsd-kernel.yaml +14 -0
  204. package/apps/jsmdcui/runtime/syntax/fsharp.yaml +48 -0
  205. package/apps/jsmdcui/runtime/syntax/gdscript.yaml +61 -0
  206. package/apps/jsmdcui/runtime/syntax/gemini.yaml +19 -0
  207. package/apps/jsmdcui/runtime/syntax/gentoo-ebuild.yaml +48 -0
  208. package/apps/jsmdcui/runtime/syntax/gentoo-etc-portage.yaml +23 -0
  209. package/apps/jsmdcui/runtime/syntax/git-commit.yaml +35 -0
  210. package/apps/jsmdcui/runtime/syntax/git-config.yaml +14 -0
  211. package/apps/jsmdcui/runtime/syntax/git-rebase-todo.yaml +19 -0
  212. package/apps/jsmdcui/runtime/syntax/gleam.yaml +69 -0
  213. package/apps/jsmdcui/runtime/syntax/glsl.yaml +26 -0
  214. package/apps/jsmdcui/runtime/syntax/gnuplot.yaml +15 -0
  215. package/apps/jsmdcui/runtime/syntax/go.yaml +62 -0
  216. package/apps/jsmdcui/runtime/syntax/godoc.yaml +17 -0
  217. package/apps/jsmdcui/runtime/syntax/golo.yaml +73 -0
  218. package/apps/jsmdcui/runtime/syntax/gomod.yaml +31 -0
  219. package/apps/jsmdcui/runtime/syntax/graphql.yaml +47 -0
  220. package/apps/jsmdcui/runtime/syntax/groff.yaml +30 -0
  221. package/apps/jsmdcui/runtime/syntax/groovy.yaml +111 -0
  222. package/apps/jsmdcui/runtime/syntax/haml.yaml +16 -0
  223. package/apps/jsmdcui/runtime/syntax/hare.yaml +52 -0
  224. package/apps/jsmdcui/runtime/syntax/haskell.yaml +52 -0
  225. package/apps/jsmdcui/runtime/syntax/hc.yaml +52 -0
  226. package/apps/jsmdcui/runtime/syntax/html.yaml +70 -0
  227. package/apps/jsmdcui/runtime/syntax/html4.yaml +25 -0
  228. package/apps/jsmdcui/runtime/syntax/html5.yaml +25 -0
  229. package/apps/jsmdcui/runtime/syntax/ini.yaml +23 -0
  230. package/apps/jsmdcui/runtime/syntax/inputrc.yaml +14 -0
  231. package/apps/jsmdcui/runtime/syntax/java.yaml +37 -0
  232. package/apps/jsmdcui/runtime/syntax/javascript.yaml +124 -0
  233. package/apps/jsmdcui/runtime/syntax/jinja2.yaml +19 -0
  234. package/apps/jsmdcui/runtime/syntax/json.yaml +39 -0
  235. package/apps/jsmdcui/runtime/syntax/jsonnet.yaml +92 -0
  236. package/apps/jsmdcui/runtime/syntax/julia.yaml +57 -0
  237. package/apps/jsmdcui/runtime/syntax/justfile.yaml +40 -0
  238. package/apps/jsmdcui/runtime/syntax/keymap.yaml +27 -0
  239. package/apps/jsmdcui/runtime/syntax/kickstart.yaml +16 -0
  240. package/apps/jsmdcui/runtime/syntax/kotlin.yaml +66 -0
  241. package/apps/jsmdcui/runtime/syntax/kvlang.yaml +67 -0
  242. package/apps/jsmdcui/runtime/syntax/ledger.yaml +14 -0
  243. package/apps/jsmdcui/runtime/syntax/lfe.yaml +17 -0
  244. package/apps/jsmdcui/runtime/syntax/lilypond.yaml +26 -0
  245. package/apps/jsmdcui/runtime/syntax/lisp.yaml +17 -0
  246. package/apps/jsmdcui/runtime/syntax/log.yaml +92 -0
  247. package/apps/jsmdcui/runtime/syntax/lua.yaml +111 -0
  248. package/apps/jsmdcui/runtime/syntax/mail.yaml +25 -0
  249. package/apps/jsmdcui/runtime/syntax/makefile.yaml +38 -0
  250. package/apps/jsmdcui/runtime/syntax/man.yaml +12 -0
  251. package/apps/jsmdcui/runtime/syntax/markdown.yaml +64 -0
  252. package/apps/jsmdcui/runtime/syntax/mc.yaml +23 -0
  253. package/apps/jsmdcui/runtime/syntax/meson.yaml +51 -0
  254. package/apps/jsmdcui/runtime/syntax/micro.yaml +34 -0
  255. package/apps/jsmdcui/runtime/syntax/mpdconf.yaml +13 -0
  256. package/apps/jsmdcui/runtime/syntax/msbuild.yaml +6 -0
  257. package/apps/jsmdcui/runtime/syntax/nanorc.yaml +16 -0
  258. package/apps/jsmdcui/runtime/syntax/nftables.yaml +30 -0
  259. package/apps/jsmdcui/runtime/syntax/nginx.yaml +22 -0
  260. package/apps/jsmdcui/runtime/syntax/nim.yaml +27 -0
  261. package/apps/jsmdcui/runtime/syntax/nix.yaml +32 -0
  262. package/apps/jsmdcui/runtime/syntax/nu.yaml +114 -0
  263. package/apps/jsmdcui/runtime/syntax/objc.yaml +60 -0
  264. package/apps/jsmdcui/runtime/syntax/ocaml.yaml +43 -0
  265. package/apps/jsmdcui/runtime/syntax/octave.yaml +83 -0
  266. package/apps/jsmdcui/runtime/syntax/odin.yaml +64 -0
  267. package/apps/jsmdcui/runtime/syntax/pascal.yaml +45 -0
  268. package/apps/jsmdcui/runtime/syntax/patch.yaml +14 -0
  269. package/apps/jsmdcui/runtime/syntax/peg.yaml +16 -0
  270. package/apps/jsmdcui/runtime/syntax/perl.yaml +58 -0
  271. package/apps/jsmdcui/runtime/syntax/php.yaml +60 -0
  272. package/apps/jsmdcui/runtime/syntax/pkg-config.yaml +12 -0
  273. package/apps/jsmdcui/runtime/syntax/po.yaml +12 -0
  274. package/apps/jsmdcui/runtime/syntax/pony.yaml +37 -0
  275. package/apps/jsmdcui/runtime/syntax/pov.yaml +21 -0
  276. package/apps/jsmdcui/runtime/syntax/privoxy-action.yaml +14 -0
  277. package/apps/jsmdcui/runtime/syntax/privoxy-config.yaml +10 -0
  278. package/apps/jsmdcui/runtime/syntax/privoxy-filter.yaml +12 -0
  279. package/apps/jsmdcui/runtime/syntax/proto.yaml +40 -0
  280. package/apps/jsmdcui/runtime/syntax/prql.yaml +84 -0
  281. package/apps/jsmdcui/runtime/syntax/puppet.yaml +22 -0
  282. package/apps/jsmdcui/runtime/syntax/python2.yaml +60 -0
  283. package/apps/jsmdcui/runtime/syntax/python3.yaml +80 -0
  284. package/apps/jsmdcui/runtime/syntax/r.yaml +32 -0
  285. package/apps/jsmdcui/runtime/syntax/raku.yaml +42 -0
  286. package/apps/jsmdcui/runtime/syntax/reST.yaml +18 -0
  287. package/apps/jsmdcui/runtime/syntax/renpy.yaml +15 -0
  288. package/apps/jsmdcui/runtime/syntax/rpmspec.yaml +43 -0
  289. package/apps/jsmdcui/runtime/syntax/ruby.yaml +73 -0
  290. package/apps/jsmdcui/runtime/syntax/rust.yaml +78 -0
  291. package/apps/jsmdcui/runtime/syntax/sage.yaml +60 -0
  292. package/apps/jsmdcui/runtime/syntax/scad.yaml +53 -0
  293. package/apps/jsmdcui/runtime/syntax/scala.yaml +33 -0
  294. package/apps/jsmdcui/runtime/syntax/sed.yaml +13 -0
  295. package/apps/jsmdcui/runtime/syntax/sh.yaml +69 -0
  296. package/apps/jsmdcui/runtime/syntax/sls.yaml +15 -0
  297. package/apps/jsmdcui/runtime/syntax/smalltalk.yaml +55 -0
  298. package/apps/jsmdcui/runtime/syntax/solidity.yaml +41 -0
  299. package/apps/jsmdcui/runtime/syntax/sql.yaml +35 -0
  300. package/apps/jsmdcui/runtime/syntax/stata.yaml +67 -0
  301. package/apps/jsmdcui/runtime/syntax/svelte.yaml +27 -0
  302. package/apps/jsmdcui/runtime/syntax/swift.yaml +103 -0
  303. package/apps/jsmdcui/runtime/syntax/systemd.yaml +16 -0
  304. package/apps/jsmdcui/runtime/syntax/tcl.yaml +18 -0
  305. package/apps/jsmdcui/runtime/syntax/terraform.yaml +87 -0
  306. package/apps/jsmdcui/runtime/syntax/tex.yaml +32 -0
  307. package/apps/jsmdcui/runtime/syntax/toml.yaml +56 -0
  308. package/apps/jsmdcui/runtime/syntax/twig.yaml +55 -0
  309. package/apps/jsmdcui/runtime/syntax/typescript.yaml +49 -0
  310. package/apps/jsmdcui/runtime/syntax/v.yaml +80 -0
  311. package/apps/jsmdcui/runtime/syntax/vala.yaml +26 -0
  312. package/apps/jsmdcui/runtime/syntax/verilog.yaml +60 -0
  313. package/apps/jsmdcui/runtime/syntax/vhdl.yaml +37 -0
  314. package/apps/jsmdcui/runtime/syntax/vi.yaml +31 -0
  315. package/apps/jsmdcui/runtime/syntax/vue.yaml +64 -0
  316. package/apps/jsmdcui/runtime/syntax/xml.yaml +37 -0
  317. package/apps/jsmdcui/runtime/syntax/xresources.yaml +14 -0
  318. package/apps/jsmdcui/runtime/syntax/yaml.yaml +34 -0
  319. package/apps/jsmdcui/runtime/syntax/yum.yaml +12 -0
  320. package/apps/jsmdcui/runtime/syntax/zig.yaml +52 -0
  321. package/apps/jsmdcui/runtime/syntax/zscript.yaml +72 -0
  322. package/apps/jsmdcui/runtime/syntax/zsh.yaml +52 -0
  323. package/apps/jsmdcui/single-exe/README.md +473 -0
  324. package/apps/jsmdcui/single-exe/assetsHelper.js +191 -0
  325. package/apps/jsmdcui/single-exe/assetsLoader.mjs +85 -0
  326. package/apps/jsmdcui/single-exe/compiled.js +212 -0
  327. package/apps/jsmdcui/single-exe/entry.mjs +10 -0
  328. package/apps/jsmdcui/single-exe/packAssets.sh +7 -0
  329. package/apps/jsmdcui/src/MDCUI_DEFAULT_EDIT +0 -0
  330. package/apps/jsmdcui/src/buffer/backup.js +165 -0
  331. package/apps/jsmdcui/src/buffer/buffer.js +126 -0
  332. package/apps/jsmdcui/src/buffer/fixed3-codec.js +140 -0
  333. package/apps/jsmdcui/src/buffer/loc.js +38 -0
  334. package/apps/jsmdcui/src/buffer/message.js +29 -0
  335. package/apps/jsmdcui/src/build-args.js +40 -0
  336. package/apps/jsmdcui/src/config/clean.js +172 -0
  337. package/apps/jsmdcui/src/config/colorscheme.js +99 -0
  338. package/apps/jsmdcui/src/config/config.js +125 -0
  339. package/apps/jsmdcui/src/config/defaults.js +109 -0
  340. package/apps/jsmdcui/src/cui/fence-events.mjs +144 -0
  341. package/apps/jsmdcui/src/cui/heading-ids.mjs +1568 -0
  342. package/apps/jsmdcui/src/cui/id-collision.mjs +111 -0
  343. package/apps/jsmdcui/src/cui/identity.mjs +26 -0
  344. package/apps/jsmdcui/src/cui/kitty-debug.mjs +25 -0
  345. package/apps/jsmdcui/src/cui/kitty-images.mjs +280 -0
  346. package/apps/jsmdcui/src/cui/kitty-mode.mjs +6 -0
  347. package/apps/jsmdcui/src/cui/rerender-queue.mjs +66 -0
  348. package/apps/jsmdcui/src/cui/rpc.mjs +1525 -0
  349. package/apps/jsmdcui/src/cui/server.mjs +128 -0
  350. package/apps/jsmdcui/src/cui/table-render.mjs +286 -0
  351. package/apps/jsmdcui/src/cui/table-row-edit.mjs +226 -0
  352. package/apps/jsmdcui/src/cui/task-checkbox.mjs +60 -0
  353. package/apps/jsmdcui/src/cui/template-components.mjs +253 -0
  354. package/apps/jsmdcui/src/cui/text-block-resize.mjs +177 -0
  355. package/apps/jsmdcui/src/cui/tui-links.mjs +560 -0
  356. package/apps/jsmdcui/src/display/ansi-style.js +60 -0
  357. package/apps/jsmdcui/src/highlight/highlighter.js +243 -0
  358. package/apps/jsmdcui/src/highlight/parser.js +225 -0
  359. package/apps/jsmdcui/src/index.js +9573 -0
  360. package/apps/jsmdcui/src/lua/engine.js +68 -0
  361. package/apps/jsmdcui/src/platform/archive.js +50 -0
  362. package/apps/jsmdcui/src/platform/clipboard.js +278 -0
  363. package/apps/jsmdcui/src/platform/commands.js +221 -0
  364. package/apps/jsmdcui/src/platform/terminal.js +5 -0
  365. package/apps/jsmdcui/src/plugins/js-bridge.js +3377 -0
  366. package/apps/jsmdcui/src/plugins/manager.js +674 -0
  367. package/apps/jsmdcui/src/runtime/encodings.js +10 -0
  368. package/apps/jsmdcui/src/runtime/registry.js +134 -0
  369. package/apps/jsmdcui/src/screen/cell-buffer.js +81 -0
  370. package/apps/jsmdcui/src/screen/events.js +263 -0
  371. package/apps/jsmdcui/src/screen/screen.js +249 -0
  372. package/apps/jsmdcui/src/screen/vt100.js +571 -0
  373. package/apps/jsmdcui/src/shell/shell.js +70 -0
  374. package/apps/jsmdcui/testapp.md +78 -0
  375. package/apps/jsmdcui/tests/assets-helper.test.js +150 -0
  376. package/apps/jsmdcui/tests/backup.test.js +158 -0
  377. package/apps/jsmdcui/tests/cat-markdown.test.js +139 -0
  378. package/apps/jsmdcui/tests/cmphex3b64.js +95 -0
  379. package/apps/jsmdcui/tests/compiled-runtime.test.js +139 -0
  380. package/apps/jsmdcui/tests/config-settings.test.js +52 -0
  381. package/apps/jsmdcui/tests/demo.test.js +447 -0
  382. package/apps/jsmdcui/tests/docs-sync.test.js +14 -0
  383. package/apps/jsmdcui/tests/fence-events.test.js +1293 -0
  384. package/apps/jsmdcui/tests/heading-ids.test.js +559 -0
  385. package/apps/jsmdcui/tests/heading-list-selector.test.js +2393 -0
  386. package/apps/jsmdcui/tests/id-collision.test.js +514 -0
  387. package/apps/jsmdcui/tests/js-plugin-prompts.test.js +110 -0
  388. package/apps/jsmdcui/tests/key-event.md +10 -0
  389. package/apps/jsmdcui/tests/kitty-demo.md +184 -0
  390. package/apps/jsmdcui/tests/kitty-images.test.js +288 -0
  391. package/apps/jsmdcui/tests/kitty-mode.test.js +14 -0
  392. package/apps/jsmdcui/tests/platform-commands.test.js +18 -0
  393. package/apps/jsmdcui/tests/platform-terminal.test.js +8 -0
  394. package/apps/jsmdcui/tests/pty-demo.js +492 -0
  395. package/apps/jsmdcui/tests/rerender-queue.test.js +130 -0
  396. package/apps/jsmdcui/tests/table-render.test.js +150 -0
  397. package/apps/jsmdcui/tests/table-row-edit.test.js +138 -0
  398. package/apps/jsmdcui/tests/task-checkbox.test.js +33 -0
  399. package/apps/jsmdcui/tests/template-components.test.js +422 -0
  400. package/apps/jsmdcui/tests/text-block-resize.test.js +222 -0
  401. package/apps/jsmdcui/tests/textarea.md +8 -0
  402. package/apps/jsmdcui/tests/tui-links.test.js +298 -0
  403. package/apps/jsmdcui/tests/wui-responsive-images.test.js +60 -0
  404. package/apps/jsmdcui/tests/wui.test.js +108 -0
  405. package/apps/jsmdcui/tests/wv-client.js +96 -0
  406. package/apps/jsmdcui/tui +7 -0
  407. package/apps/jsmdcui/wui +9 -0
  408. package/apps/musl-la/LICENSES/Apache-2.0_LLVM_Exceptions_license.txt +311 -0
  409. package/apps/musl-la/LICENSES/Apache-2.0_license.txt +177 -0
  410. package/apps/musl-la/LICENSES/BSD-3-Clause_license.txt +43 -0
  411. package/apps/musl-la/LICENSES/GPL-3.0.txt +674 -0
  412. package/apps/musl-la/LICENSES/LGPL-3.0.txt +165 -0
  413. package/apps/musl-la/LICENSES/MIT.txt +21 -0
  414. package/apps/musl-la/LICENSES/curl_license.txt +22 -0
  415. package/apps/musl-la/LICENSES/gcc_runtime_exception.txt +73 -0
  416. package/apps/musl-la/LICENSES/zlib_license.txt +22 -0
  417. package/apps/musl-la/LICENSE_musl.txt +193 -0
  418. package/apps/musl-la/NOTICE +56 -0
  419. package/apps/musl-la/ldmusl +0 -0
  420. package/apps/musl-la/libgcc_s.so.1 +0 -0
  421. package/apps/musl-la/libstdc++.so.6 +0 -0
  422. package/apps/musl-la/musl-la.sh +5 -0
  423. package/bin/bun.sh +57 -0
  424. package/bin/glow.sh +5 -0
  425. package/bin/init.js +395 -0
  426. package/bin/jmi.sh +5 -0
  427. package/bin/jsgotty.sh +5 -0
  428. package/bin/libsh-loader.c +135 -0
  429. package/bin/libsh-loader.so +0 -0
  430. package/bin/multicall.sh +90 -0
  431. package/bin/shc.js +5 -0
  432. package/bin/switch_to_android.sh +9 -0
  433. package/bin/switch_to_linux.sh +7 -0
  434. package/icon.png +0 -0
  435. package/package.json +31 -0
@@ -0,0 +1,1293 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fenceEventMap, inlineFenceEventCode, parseFenceDeclarations } from "../src/cui/fence-events.mjs";
6
+ import { evalFront, installWebDollar, runWebMdcuiLoad } from "../src/cui/rpc.mjs";
7
+ import { buildTuiBlockIndex, createTuiSelector, findTuiBlockAtLine, findTuiBlockInIndex, insertTuiTextareaNewline, mergeTuiTextareaBackward, mergeTuiTextareaForward } from "../src/plugins/js-bridge.js";
8
+ import { convertWuiTextareas, wrapWuiHeadingSections } from "../runmd.mjs";
9
+
10
+ const tui = join(import.meta.dir, "..", "tui");
11
+ const bunBin = Bun.which("bun") || process.argv0;
12
+
13
+ async function waitFor(check, timeout = 5000) {
14
+ const deadline = Date.now() + timeout;
15
+ while (Date.now() < deadline) {
16
+ if (check()) return;
17
+ await Bun.sleep(20);
18
+ }
19
+ throw new Error("timed out waiting for PTY output");
20
+ }
21
+
22
+ test("fenced text controls retain quoted inline keydown code after the identity", () => {
23
+ const markdown = '```text#myid.field @keydown="first(); second(event); show(\\"done\\")"\nvalue\n```\n';
24
+ const declarations = parseFenceDeclarations(markdown);
25
+ expect(declarations).toHaveLength(1);
26
+ expect(declarations[0]).toMatchObject({
27
+ tag: "text",
28
+ id: "myid",
29
+ classes: ["field"],
30
+ });
31
+ expect(declarations[0].events.get("keydown")).toEqual({
32
+ code: 'first(); second(event); show("done")',
33
+ modifiers: [],
34
+ });
35
+ });
36
+
37
+ test("fence discovery follows Markdown container and raw HTML boundaries", () => {
38
+ const markdown = `> \`\`\`text#quote
39
+ > value
40
+ > \`\`\`
41
+
42
+ - \`\`\`textarea#list
43
+ value
44
+ \`\`\`
45
+
46
+ <div>
47
+ \`\`\`text#raw-div
48
+ value
49
+ \`\`\`
50
+ </div>
51
+
52
+ <!--
53
+ \`\`\`text#comment
54
+ value
55
+ \`\`\`
56
+ -->
57
+ `;
58
+ expect(
59
+ parseFenceDeclarations(markdown).map(({ id, line }) => ({ id, line })),
60
+ ).toEqual([
61
+ { id: "quote", line: 1 },
62
+ { id: "list", line: 5 },
63
+ ]);
64
+ });
65
+
66
+ test("Unicode fenced-control IDs work in declarations, WUI output, and TUI selectors", () => {
67
+ const markdown = '```text#中文-欄位.field @keydown.prevent="guard(event)"\nvalue\n```\n';
68
+ const declarations = parseFenceDeclarations(markdown);
69
+ expect(declarations).toHaveLength(1);
70
+ expect(declarations[0]).toMatchObject({
71
+ tag: "text",
72
+ id: "中文-欄位",
73
+ classes: ["field"],
74
+ });
75
+ expect(declarations[0].events.get("keydown")).toEqual({
76
+ code: "guard(event)",
77
+ modifiers: ["prevent"],
78
+ });
79
+
80
+ const html = convertWuiTextareas(
81
+ Bun.markdown.html(markdown),
82
+ fenceEventMap(markdown),
83
+ );
84
+ expect(html).toContain('id="中文-欄位"');
85
+ expect(html).toContain("event.preventDefault();guard(event)");
86
+
87
+ const buffer = {
88
+ lines: ["┌─ text#中文-欄位.field", "│ value", "└─"],
89
+ };
90
+ const $ = createTuiSelector(() => buffer);
91
+ expect($("#中文-欄位").id).toBe("中文-欄位");
92
+ expect($("#中文-欄位").val()).toBe("value");
93
+ expect($({ id: "中文-欄位" }).val()).toBe("value");
94
+ });
95
+
96
+ test("prevent modifier prepends preventDefault to inline event code", () => {
97
+ const markdown = '```text#myid @keydown.prevent="submit(event)"\nvalue\n```\n';
98
+ const handler = fenceEventMap(markdown).get("myid").events.get("keydown");
99
+ expect(handler).toEqual({ code: "submit(event)", modifiers: ["prevent"] });
100
+ expect(inlineFenceEventCode(handler)).toBe("event.preventDefault();submit(event)");
101
+ });
102
+
103
+ test("WUI writes keyboard code as native inline handlers with a mobile beforeinput fallback", () => {
104
+ const markdown = '```text#myid.field @keydown.prevent="guard(event)"\nvalue\n```\n';
105
+ const html = convertWuiTextareas(Bun.markdown.html(markdown), fenceEventMap(markdown));
106
+ expect(html).toContain('id="myid"');
107
+ expect(html).toContain('const __mdcuiKeyCode=Number(event.keyCode||event.which||0);');
108
+ expect(html).toContain('if(event.key!==&quot;Unidentified&quot;){\nObject.defineProperty(event,&quot;toJSON&quot;');
109
+ expect(html).toContain('event.preventDefault();guard(event)\n}"');
110
+ expect(html).toContain('onbeforeinput="if(!this.__mdcuiIdentifiedKeydown&amp;&amp;event.data!=null');
111
+ expect(html).toContain('ctrlKey:{configurable:true,value:!!m.ctrlKey}');
112
+ expect(html).toContain('metaKey:{configurable:true,value:!!m.metaKey}');
113
+ expect(html).not.toContain("onkeyup=");
114
+ expect(html).not.toContain("addEventListener");
115
+ });
116
+
117
+ test("input handlers are added without changing keydown or mobile beforeinput", () => {
118
+ const markdown = '```text#myid @keydown="guard(event)" @input="changed(event)"\nvalue\n```\n';
119
+ const declaration = fenceEventMap(markdown).get("myid");
120
+ const html = convertWuiTextareas(Bun.markdown.html(markdown), fenceEventMap(markdown));
121
+ expect(declaration.events.get("input")).toEqual({
122
+ code: "changed(event)",
123
+ modifiers: [],
124
+ });
125
+ expect(html).toContain("onkeydown=");
126
+ expect(html).toContain("onbeforeinput=");
127
+ expect(html).toContain('oninput="changed(event)"');
128
+ });
129
+
130
+ test("WUI onMdcuiLoad waits for window load and runs once", async () => {
131
+ let loadListener = null;
132
+ let calls = 0;
133
+ const target = {
134
+ document: {
135
+ readyState: "loading",
136
+ getElementById() { return null; },
137
+ querySelectorAll() { return []; },
138
+ addEventListener() {},
139
+ },
140
+ addEventListener(name, listener) {
141
+ if (name === "load") loadListener = listener;
142
+ },
143
+ console,
144
+ };
145
+ const frontMod = {
146
+ onMdcuiLoad() { calls++; },
147
+ };
148
+
149
+ const pending = runWebMdcuiLoad(target, frontMod);
150
+ await Bun.sleep(0);
151
+ expect(calls).toBe(0);
152
+ loadListener();
153
+ await pending;
154
+ expect(calls).toBe(1);
155
+ await runWebMdcuiLoad(target, frontMod);
156
+ expect(calls).toBe(1);
157
+ });
158
+
159
+ test("WUI $.tts resolves after browser speech ends with pitch and speed", async () => {
160
+ const spoken = [];
161
+ let cancellations = 0;
162
+ class Utterance {
163
+ constructor(text) { this.text = text; }
164
+ }
165
+ const target = {
166
+ document: {
167
+ getElementById() { return null; },
168
+ querySelectorAll() { return []; },
169
+ addEventListener() {},
170
+ },
171
+ addEventListener() {},
172
+ SpeechSynthesisUtterance: Utterance,
173
+ speechSynthesis: {
174
+ cancel() { cancellations++; },
175
+ speak(utterance) {
176
+ spoken.push(utterance);
177
+ queueMicrotask(() => utterance.onend());
178
+ },
179
+ },
180
+ };
181
+ installWebDollar(target);
182
+ await target.$.tts("Hello world", 1.4, 0.7);
183
+ expect(spoken).toHaveLength(1);
184
+ expect(spoken[0]).toMatchObject({
185
+ text: "Hello world",
186
+ pitch: 1.4,
187
+ rate: 0.7,
188
+ lang: "en-US",
189
+ });
190
+ expect(target.$.tts.stop()).toBe(true);
191
+ expect(cancellations).toBe(2);
192
+ });
193
+
194
+ test("WUI $.tts resolves error reasons instead of rejecting", async () => {
195
+ const document = {
196
+ getElementById() { return null; },
197
+ querySelectorAll() { return []; },
198
+ addEventListener() {},
199
+ };
200
+ const unavailable = { document, addEventListener() {} };
201
+ installWebDollar(unavailable);
202
+ expect(await unavailable.$.tts("hello")).toBe(
203
+ "Web Speech synthesis is unavailable",
204
+ );
205
+
206
+ class Utterance {
207
+ constructor(text) { this.text = text; }
208
+ }
209
+ const failed = {
210
+ document,
211
+ addEventListener() {},
212
+ SpeechSynthesisUtterance: Utterance,
213
+ speechSynthesis: {
214
+ cancel() {},
215
+ speak(utterance) {
216
+ queueMicrotask(() => utterance.onerror({ error: "not-allowed" }));
217
+ },
218
+ },
219
+ };
220
+ installWebDollar(failed);
221
+ expect(await failed.$.tts("hello")).toBe(
222
+ "Speech synthesis failed: not-allowed",
223
+ );
224
+ });
225
+
226
+ test("keyup declarations are unsupported in both interfaces", () => {
227
+ const markdown = '```text#myid @keyup="update(event)"\nvalue\n```\n';
228
+ const declarations = parseFenceDeclarations(markdown);
229
+ const html = convertWuiTextareas(Bun.markdown.html(markdown), fenceEventMap(markdown));
230
+ expect(declarations[0].events.size).toBe(0);
231
+ expect(fenceEventMap(markdown).has("myid")).toBeFalse();
232
+ expect(html).not.toContain("onkeyup=");
233
+ expect(html).not.toContain("onkeydown=");
234
+ });
235
+
236
+ test("WUI keeps a trailing line comment inside the generated keydown block", () => {
237
+ const markdown = '```text#myid @keydown="guard(event); // trailing comment"\nvalue\n```\n';
238
+ const html = convertWuiTextareas(Bun.markdown.html(markdown), fenceEventMap(markdown));
239
+ const code = (html.match(/onkeydown="([^"]*)"/)?.[1] ?? "")
240
+ .replaceAll("&quot;", '"')
241
+ .replaceAll("&gt;", ">")
242
+ .replaceAll("&lt;", "<")
243
+ .replaceAll("&amp;", "&");
244
+ expect(() => new Function("event", "guard", code)).not.toThrow();
245
+ });
246
+
247
+ test("WUI restores modifier letters from keyCode or code during keydown", () => {
248
+ const markdown = '```text#myid @keydown.prevent="guard(event)"\nvalue\n```\n';
249
+ const html = convertWuiTextareas(Bun.markdown.html(markdown), fenceEventMap(markdown));
250
+ const decodeAttribute = (value) => value
251
+ .replaceAll("&quot;", '"')
252
+ .replaceAll("&gt;", ">")
253
+ .replaceAll("&lt;", "<")
254
+ .replaceAll("&amp;", "&");
255
+ const keydownCode = decodeAttribute(html.match(/onkeydown="([^"]*)"/)?.[1] ?? "");
256
+ const beforeInputCode = decodeAttribute(html.match(/onbeforeinput="([^"]*)"/)?.[1] ?? "");
257
+ const seen = [];
258
+ const element = {};
259
+ const runKeydown = new Function("event", "guard", keydownCode);
260
+ element.onkeydown = (event) => runKeydown.call(element, event, (current) => seen.push({
261
+ key: current.key,
262
+ ctrlKey: current.ctrlKey,
263
+ shiftKey: current.shiftKey,
264
+ altKey: current.altKey,
265
+ metaKey: current.metaKey,
266
+ json: JSON.parse(JSON.stringify(current)),
267
+ toJSONEnumerable: Object.keys(current).includes("toJSON"),
268
+ }));
269
+
270
+ const unidentified = {
271
+ key: "ß",
272
+ keyCode: 0,
273
+ code: "KeyS",
274
+ ctrlKey: true,
275
+ shiftKey: false,
276
+ altKey: true,
277
+ metaKey: true,
278
+ defaultPrevented: false,
279
+ preventDefault() { this.defaultPrevented = true; },
280
+ };
281
+ element.onkeydown(unidentified);
282
+ expect(unidentified.defaultPrevented).toBeTrue();
283
+ expect(unidentified.key).toBe("s");
284
+
285
+ const beforeInput = {
286
+ data: "β",
287
+ defaultPrevented: false,
288
+ preventDefault() { this.defaultPrevented = true; },
289
+ };
290
+ new Function("event", beforeInputCode).call(element, beforeInput);
291
+ expect(seen).toEqual([{
292
+ key: "s",
293
+ ctrlKey: true,
294
+ shiftKey: false,
295
+ altKey: true,
296
+ metaKey: true,
297
+ json: {
298
+ type: "",
299
+ key: "s",
300
+ code: "KeyS",
301
+ raw: "",
302
+ ctrlKey: true,
303
+ shiftKey: false,
304
+ altKey: true,
305
+ metaKey: true,
306
+ repeat: false,
307
+ defaultPrevented: true,
308
+ target: { id: "", tagName: "", className: "", value: "" },
309
+ },
310
+ toJSONEnumerable: false,
311
+ }]);
312
+ expect(beforeInput.key).toBeUndefined();
313
+ expect(beforeInput.defaultPrevented).toBeFalse();
314
+ clearTimeout(element.__mdcuiKeydownReset);
315
+ });
316
+
317
+ test("WUI beforeinput keeps composed text for AltGraph and unmodified input", () => {
318
+ const markdown = '```text#myid @keydown="guard(event)"\nvalue\n```\n';
319
+ const html = convertWuiTextareas(Bun.markdown.html(markdown), fenceEventMap(markdown));
320
+ const decodeAttribute = (value) => value
321
+ .replaceAll("&quot;", '"')
322
+ .replaceAll("&gt;", ">")
323
+ .replaceAll("&lt;", "<")
324
+ .replaceAll("&amp;", "&");
325
+ const keydownCode = decodeAttribute(html.match(/onkeydown="([^"]*)"/)?.[1] ?? "");
326
+ const beforeInputCode = decodeAttribute(html.match(/onbeforeinput="([^"]*)"/)?.[1] ?? "");
327
+ const seen = [];
328
+ const element = {};
329
+ const runKeydown = new Function("event", "guard", keydownCode);
330
+ element.onkeydown = (event) => runKeydown.call(element, event, current => seen.push(current.key));
331
+ const retry = (keydown, data) => {
332
+ element.onkeydown({ key: "Unidentified", ...keydown });
333
+ new Function("event", beforeInputCode).call(element, { data });
334
+ };
335
+
336
+ retry({ keyCode: 83, altKey: true, getModifierState: name => name === "AltGraph" }, "β");
337
+ retry({ keyCode: 83 }, "絲");
338
+
339
+ expect(seen).toEqual(["β", "絲"]);
340
+ clearTimeout(element.__mdcuiKeydownReset);
341
+ });
342
+
343
+ test("TUI finds the event target only while the cursor is inside the framed body", () => {
344
+ const lines = ["┌─ text#myid.field", "│ value", "└─"];
345
+ expect(findTuiBlockAtLine(lines, 0)).toBeNull();
346
+ expect(findTuiBlockAtLine(lines, 1)?.header).toMatchObject({ tag: "text", id: "myid" });
347
+ expect(findTuiBlockAtLine(lines, 2)).toBeNull();
348
+ });
349
+
350
+ test("TUI textarea val changes participate in undo and redo", () => {
351
+ const buffer = {
352
+ lines: ["┌─ textarea#field", "│ old", "└─"],
353
+ cursor: { x: 2, y: 1 },
354
+ undoStack: [],
355
+ redoStack: [],
356
+ pushUndo(force) {
357
+ expect(force).toBeTrue();
358
+ this.undoStack.push(this.lines.slice());
359
+ this.redoStack = [];
360
+ },
361
+ undo() {
362
+ this.redoStack.push(this.lines.slice());
363
+ this.lines = this.undoStack.pop();
364
+ },
365
+ redo() {
366
+ this.undoStack.push(this.lines.slice());
367
+ this.lines = this.redoStack.pop();
368
+ },
369
+ invalidateHighlightFrom() {},
370
+ ensureCursor() {},
371
+ };
372
+ const $ = createTuiSelector(() => buffer);
373
+
374
+ $("textarea#field").val("new\nrow");
375
+ expect($("textarea#field").val()).toBe("new\nrow");
376
+ buffer.undo();
377
+ expect($("textarea#field").val()).toBe("old");
378
+ buffer.redo();
379
+ expect($("textarea#field").val()).toBe("new\nrow");
380
+ });
381
+
382
+ test("TUI textarea Enter splits a body row and Backspace joins it", () => {
383
+ const buffer = {
384
+ lines: ["┌─ textarea#field", "│ abcd", "└─", "after"],
385
+ cursor: { x: 4, y: 1 },
386
+ _mdcuiFenceBlockIndex: { stale: true },
387
+ invalidateHighlightFrom() {},
388
+ ensureCursor() {},
389
+ };
390
+ const block = findTuiBlockAtLine(buffer.lines, buffer.cursor.y);
391
+
392
+ expect(insertTuiTextareaNewline(buffer, block)).toBeTrue();
393
+ expect(buffer.lines).toEqual([
394
+ "┌─ textarea#field",
395
+ "│ ab",
396
+ "│ cd",
397
+ "└─",
398
+ "after",
399
+ ]);
400
+ expect(buffer.cursor).toEqual({ x: 2, y: 2 });
401
+ expect(buffer._mdcuiFenceBlockIndex).toBeNull();
402
+
403
+ expect(mergeTuiTextareaBackward(buffer, findTuiBlockAtLine(buffer.lines, buffer.cursor.y))).toBeTrue();
404
+ expect(buffer.lines).toEqual(["┌─ textarea#field", "│ abcd", "└─", "after"]);
405
+ expect(buffer.cursor).toEqual({ x: 4, y: 1 });
406
+ });
407
+
408
+ test("TUI textarea Delete at line end joins the next body row", () => {
409
+ const buffer = {
410
+ lines: ["┌─ textarea#field", "│ ab", "│ cd", "└─", "after"],
411
+ cursor: { x: 4, y: 1 },
412
+ _mdcuiFenceBlockIndex: { stale: true },
413
+ _mdcuiControlBlockIndex: { stale: true },
414
+ invalidateHighlightFrom() {},
415
+ ensureCursor() {},
416
+ };
417
+ const block = findTuiBlockAtLine(buffer.lines, buffer.cursor.y);
418
+
419
+ expect(mergeTuiTextareaForward(buffer, block)).toBeTrue();
420
+ expect(buffer.lines).toEqual(["┌─ textarea#field", "│ abcd", "└─", "after"]);
421
+ expect(buffer.cursor).toEqual({ x: 4, y: 1 });
422
+ expect(buffer._mdcuiFenceBlockIndex).toBeNull();
423
+ expect(buffer._mdcuiControlBlockIndex).toBeNull();
424
+ expect(mergeTuiTextareaForward(buffer, findTuiBlockAtLine(buffer.lines, buffer.cursor.y))).toBeFalse();
425
+ });
426
+
427
+ test("TUI keyboard lookup indexes event blocks once and uses binary lookup", () => {
428
+ const lines = [
429
+ "heading",
430
+ "┌─ text#plain",
431
+ "│ no event",
432
+ "└─",
433
+ ...Array.from({ length: 5_000 }, (_, index) => `line ${index}`),
434
+ "┌─ text#target",
435
+ "│ event body",
436
+ "└─",
437
+ ];
438
+ const declarations = new Map([
439
+ ["target", { tag: "text", events: new Map([["keydown", { code: "hit()" }]]) }],
440
+ ]);
441
+ const blocks = buildTuiBlockIndex(lines, declarations);
442
+
443
+ expect(blocks).toHaveLength(1);
444
+ expect(blocks[0].header).toMatchObject({ tag: "text", id: "target" });
445
+ expect(findTuiBlockInIndex(blocks, lines.length - 2)).toBe(blocks[0]);
446
+ expect(findTuiBlockInIndex(blocks, 2)).toBeNull();
447
+ });
448
+
449
+ test("front evaluation exposes the TUI event scope to inline statements", async () => {
450
+ const seen = [];
451
+ const event = { key: "Enter" };
452
+ const result = await evalFront(
453
+ { record(value) { seen.push(value); } },
454
+ "record(event.key)",
455
+ { event },
456
+ );
457
+ expect(result).toBeUndefined();
458
+ expect(seen).toEqual(["Enter"]);
459
+ });
460
+
461
+ test("front evaluation binds link this and event targets to the same object", async () => {
462
+ const $ = createTuiSelector(() => null);
463
+ const target = {
464
+ tagName: "A",
465
+ href: "javascript:inspect(event)",
466
+ textContent: "Save",
467
+ };
468
+ const event = { target, currentTarget: target };
469
+ const result = await evalFront(
470
+ {
471
+ inspect(current) {
472
+ return {
473
+ thisText: this?.textContent,
474
+ eventText: current.target.textContent,
475
+ sameTarget: current.target === current.currentTarget,
476
+ };
477
+ },
478
+ },
479
+ "javascript:({ direct: $(this).text(), called: inspect.call(this, event) })",
480
+ { event, target, $ },
481
+ target,
482
+ );
483
+
484
+ expect(result).toEqual({
485
+ direct: "Save",
486
+ called: {
487
+ thisText: "Save",
488
+ eventText: "Save",
489
+ sameTarget: true,
490
+ },
491
+ });
492
+ });
493
+
494
+ test("front evaluation ignores module exports that cannot be local bindings", async () => {
495
+ const result = await evalFront(
496
+ {
497
+ default: "ignored",
498
+ "not-valid": "ignored",
499
+ await: "ignored",
500
+ inspect() { return "called"; },
501
+ },
502
+ "javascript:inspect()",
503
+ );
504
+
505
+ expect(result).toBe("called");
506
+ });
507
+
508
+ test("front evaluation preserves raw percent sequences outside the WUI href boundary", async () => {
509
+ const inspect = value => value;
510
+
511
+ expect(await evalFront(
512
+ { inspect },
513
+ 'javascript:inspect("hello%20world")',
514
+ )).toBe("hello%20world");
515
+ expect(await evalFront(
516
+ { inspect },
517
+ "javascript:inspect(100%25)",
518
+ )).toBe(0);
519
+ });
520
+
521
+ test("WUI javascript href interception injects the registered front module, this, and event", async () => {
522
+ let clickListener;
523
+ let seen;
524
+ let preventDefaultCalls = 0;
525
+ const anchor = {
526
+ tagName: "A",
527
+ href: "javascript:inspect(this,event)",
528
+ getAttribute(name) {
529
+ return name === "href" ? this.href : null;
530
+ },
531
+ closest(selector) {
532
+ return selector === "a[href]" ? this : null;
533
+ },
534
+ };
535
+ const documentObject = {
536
+ readyState: "complete",
537
+ addEventListener(name, listener) {
538
+ if (name === "click") clickListener = listener;
539
+ },
540
+ querySelector() { return null; },
541
+ querySelectorAll() { return []; },
542
+ };
543
+ const target = {
544
+ document: documentObject,
545
+ addEventListener() {},
546
+ __mdcuiFrontModule: {
547
+ inspect(targetArg, eventArg) {
548
+ seen = {
549
+ initialDefaultPrevented: eventArg.defaultPrevented,
550
+ targetArg,
551
+ eventArg,
552
+ target: eventArg.target,
553
+ currentTarget: eventArg.currentTarget,
554
+ type: eventArg.type,
555
+ };
556
+ eventArg.preventDefault();
557
+ seen.afterPreventDefault = eventArg.defaultPrevented;
558
+ },
559
+ },
560
+ };
561
+ installWebDollar(target);
562
+ const nativeEvent = {
563
+ type: "click",
564
+ target: anchor,
565
+ defaultPrevented: false,
566
+ preventDefault() {
567
+ preventDefaultCalls++;
568
+ this.defaultPrevented = true;
569
+ },
570
+ };
571
+
572
+ await clickListener(nativeEvent);
573
+
574
+ expect(seen).toEqual({
575
+ initialDefaultPrevented: false,
576
+ targetArg: anchor,
577
+ eventArg: expect.any(Object),
578
+ target: anchor,
579
+ currentTarget: anchor,
580
+ type: "click",
581
+ afterPreventDefault: true,
582
+ });
583
+ expect(nativeEvent.defaultPrevented).toBeTrue();
584
+ expect(preventDefaultCalls).toBe(2);
585
+ });
586
+
587
+ test("WUI decodes Bun-encoded quotes but preserves valid raw modulo source", async () => {
588
+ const html = Bun.markdown.html('[Inspect](javascript:inspect("hello"))');
589
+ const href = html.match(/href="([^"]+)"/)?.[1] ?? "";
590
+ expect(href).toBe("javascript:inspect(%22hello%22)");
591
+ const moduloHtml = Bun.markdown.html("[Inspect](javascript:inspect(100%25))");
592
+ const moduloHref = moduloHtml.match(/href="([^"]+)"/)?.[1] ?? "";
593
+ expect(moduloHref).toBe("javascript:inspect(100%25)");
594
+
595
+ let clickListener;
596
+ const seen = [];
597
+ const anchor = {
598
+ href,
599
+ getAttribute(name) {
600
+ return name === "href" ? this.href : null;
601
+ },
602
+ closest(selector) {
603
+ return selector === "a[href]" ? this : null;
604
+ },
605
+ };
606
+ const documentObject = {
607
+ readyState: "complete",
608
+ addEventListener(name, listener) {
609
+ if (name === "click") clickListener = listener;
610
+ },
611
+ querySelector() { return null; },
612
+ querySelectorAll() { return []; },
613
+ };
614
+ installWebDollar({
615
+ document: documentObject,
616
+ addEventListener() {},
617
+ __mdcuiFrontModule: {
618
+ inspect(value) { seen.push(value); },
619
+ },
620
+ });
621
+ const nativeEvent = {
622
+ type: "click",
623
+ target: anchor,
624
+ defaultPrevented: false,
625
+ preventDefault() {
626
+ this.defaultPrevented = true;
627
+ },
628
+ };
629
+
630
+ await clickListener(nativeEvent);
631
+ anchor.href = moduloHref;
632
+ nativeEvent.defaultPrevented = false;
633
+ await clickListener(nativeEvent);
634
+
635
+ expect(seen).toEqual(["hello", 0]);
636
+ expect(nativeEvent.defaultPrevented).toBeTrue();
637
+ });
638
+
639
+ test("WUI reports javascript link evaluation failures to its console", async () => {
640
+ let clickListener;
641
+ const errors = [];
642
+ const anchor = {
643
+ href: "javascript:fail()",
644
+ getAttribute(name) {
645
+ return name === "href" ? this.href : null;
646
+ },
647
+ closest(selector) {
648
+ return selector === "a[href]" ? this : null;
649
+ },
650
+ };
651
+ const documentObject = {
652
+ readyState: "complete",
653
+ addEventListener(name, listener) {
654
+ if (name === "click") clickListener = listener;
655
+ },
656
+ querySelector() { return null; },
657
+ querySelectorAll() { return []; },
658
+ };
659
+ installWebDollar({
660
+ document: documentObject,
661
+ addEventListener() {},
662
+ console: {
663
+ error(...args) { errors.push(args); },
664
+ },
665
+ __mdcuiFrontModule: {
666
+ fail() { throw new Error("link failed"); },
667
+ },
668
+ });
669
+ const nativeEvent = {
670
+ type: "click",
671
+ target: anchor,
672
+ defaultPrevented: false,
673
+ preventDefault() {
674
+ this.defaultPrevented = true;
675
+ },
676
+ };
677
+
678
+ await clickListener(nativeEvent);
679
+
680
+ expect(nativeEvent.defaultPrevented).toBeTrue();
681
+ expect(errors).toHaveLength(1);
682
+ expect(errors[0].join(" ")).toContain("[mdcui] javascript link: Error: link failed");
683
+ });
684
+
685
+ test("WUI reports exported function failures discarded by statement handlers", async () => {
686
+ let clickListener;
687
+ const errors = [];
688
+ const anchor = {
689
+ href: "javascript:fail();",
690
+ getAttribute(name) {
691
+ return name === "href" ? this.href : null;
692
+ },
693
+ closest(selector) {
694
+ return selector === "a[href]" ? this : null;
695
+ },
696
+ };
697
+ const documentObject = {
698
+ readyState: "complete",
699
+ addEventListener(name, listener) {
700
+ if (name === "click") clickListener = listener;
701
+ },
702
+ querySelector() { return null; },
703
+ querySelectorAll() { return []; },
704
+ };
705
+ installWebDollar({
706
+ document: documentObject,
707
+ addEventListener() {},
708
+ console: {
709
+ error(...args) { errors.push(args); },
710
+ },
711
+ __mdcuiFrontModule: {
712
+ fail() { throw new Error("sync statement failed"); },
713
+ async failAsync() {
714
+ await Promise.resolve();
715
+ throw new Error("async statement failed");
716
+ },
717
+ },
718
+ });
719
+ const nativeEvent = {
720
+ type: "click",
721
+ target: anchor,
722
+ defaultPrevented: false,
723
+ preventDefault() {
724
+ this.defaultPrevented = true;
725
+ },
726
+ };
727
+
728
+ await clickListener(nativeEvent);
729
+ anchor.href = "javascript:failAsync();";
730
+ nativeEvent.defaultPrevented = false;
731
+ await clickListener(nativeEvent);
732
+ await Promise.resolve();
733
+
734
+ expect(nativeEvent.defaultPrevented).toBeTrue();
735
+ expect(errors).toHaveLength(2);
736
+ expect(errors[0].join(" ")).toContain("sync statement failed");
737
+ expect(errors[1].join(" ")).toContain("async statement failed");
738
+ });
739
+
740
+ test("WUI javascript href interception honors an earlier preventDefault", async () => {
741
+ let clickListener;
742
+ let calls = 0;
743
+ const anchor = {
744
+ href: "javascript:inspect()",
745
+ getAttribute(name) {
746
+ return name === "href" ? this.href : null;
747
+ },
748
+ closest(selector) {
749
+ return selector === "a[href]" ? this : null;
750
+ },
751
+ };
752
+ const documentObject = {
753
+ readyState: "complete",
754
+ addEventListener(name, listener) {
755
+ if (name === "click") clickListener = listener;
756
+ },
757
+ querySelector() { return null; },
758
+ querySelectorAll() { return []; },
759
+ };
760
+ installWebDollar({
761
+ document: documentObject,
762
+ addEventListener() {},
763
+ __mdcuiFrontModule: {
764
+ inspect() { calls++; },
765
+ },
766
+ });
767
+ const nativeEvent = {
768
+ type: "click",
769
+ target: anchor,
770
+ defaultPrevented: true,
771
+ preventDefault() {
772
+ throw new Error("interceptor must not prevent an already-cancelled event");
773
+ },
774
+ };
775
+
776
+ await clickListener(nativeEvent);
777
+
778
+ expect(calls).toBe(0);
779
+ });
780
+
781
+ test("WUI wraps only the first visible heading character as a toggle target", () => {
782
+ const html = wrapWuiHeadingSections(
783
+ '<h2 id="title"><em>Title</em></h2><p>Body</p><h2 id="next">&amp; next</h2>',
784
+ );
785
+
786
+ expect(html).toContain(
787
+ '<em><span class="mdcui-heading-toggle" role="button" tabindex="0" aria-expanded="true">T</span>itle</em>',
788
+ );
789
+ expect(html).toContain(
790
+ '<span class="mdcui-heading-toggle" role="button" tabindex="0" aria-expanded="true">&amp;</span> next',
791
+ );
792
+ });
793
+
794
+ test("WUI heading toggles preserve complete HTML character references", () => {
795
+ for (const reference of [
796
+ "&copy",
797
+ "&copy;",
798
+ "&#169",
799
+ "&#xA9",
800
+ ]) {
801
+ const html = wrapWuiHeadingSections(
802
+ `<h2 id="entity">${reference} raw</h2>`,
803
+ );
804
+ expect(html).toContain(
805
+ '<span class="mdcui-heading-toggle" role="button" tabindex="0" '
806
+ + `aria-expanded="true">${reference}</span> raw`,
807
+ );
808
+ }
809
+
810
+ const bogus = wrapWuiHeadingSections(
811
+ '<h2 id="bogus">&bogus <em>x;</em> rest</h2>',
812
+ );
813
+ expect(bogus).toContain(
814
+ '<span class="mdcui-heading-toggle" role="button" tabindex="0" '
815
+ + 'aria-expanded="true">&</span>bogus <em>x;</em> rest',
816
+ );
817
+ expect(bogus).not.toContain("</span></em>");
818
+
819
+ for (const whitespace of ["&nbsp;", "&#x20;"]) {
820
+ const html = wrapWuiHeadingSections(
821
+ `<h2 id="space">${whitespace}A</h2>`,
822
+ );
823
+ expect(html).toContain(
824
+ `${whitespace}<span class="mdcui-heading-toggle" role="button" `
825
+ + 'tabindex="0" aria-expanded="true">A</span>',
826
+ );
827
+ }
828
+ });
829
+
830
+ test("WUI closes nested heading sections inside their Markdown containers", () => {
831
+ const rendered = Bun.markdown.html(`> ## Quoted
832
+ >
833
+ > Body
834
+
835
+ - ## Listed
836
+
837
+ Body
838
+
839
+ # Top
840
+ `, { headings: { ids: true } });
841
+ const html = wrapWuiHeadingSections(rendered);
842
+
843
+ expect(html).toContain(
844
+ "<blockquote>\n<section>\n<h2 id=\"quoted\">",
845
+ );
846
+ expect(html).toContain("</section>\n</blockquote>");
847
+ expect(html).toContain("<li>\n<section>\n<h2 id=\"listed\">");
848
+ expect(html).toContain("</section>\n</li>");
849
+ expect(html).not.toContain("</blockquote>\n</section>");
850
+ expect(html).not.toContain("</li>\n</section>");
851
+ });
852
+
853
+ test("raw empty-ID WUI headings fail closed before Markdown normalization", () => {
854
+ const rendered = Bun.markdown.html("# 中文\n", {
855
+ headings: { ids: true },
856
+ });
857
+ expect(rendered).toContain('<h1 id="">中文</h1>');
858
+
859
+ const html = wrapWuiHeadingSections(rendered);
860
+ expect(html).toContain('<h1 id="">中文</h1>');
861
+ expect(html).not.toContain("mdcui-heading-toggle");
862
+ expect(html).not.toContain('role="button"');
863
+ expect(html).not.toContain('tabindex="0"');
864
+ });
865
+
866
+ test("WUI heading toggles preserve quoted greater-than signs in HTML attributes", () => {
867
+ const html = wrapWuiHeadingSections(
868
+ '<!doctype html><body title="body > value">'
869
+ + '<h2 id="quoted" title="heading > value">'
870
+ + "<em data-note='inline > value'>Title</em></h2><p>Body</p>"
871
+ + "</body>",
872
+ );
873
+
874
+ expect(html).toContain('<body title="body > value">');
875
+ expect(html).toContain('<h2 id="quoted" title="heading > value">');
876
+ expect(html).toContain("<em data-note='inline > value'>"
877
+ + '<span class="mdcui-heading-toggle" role="button" tabindex="0" '
878
+ + 'aria-expanded="true">T</span>itle</em>');
879
+ expect(html).not.toContain('heading > <span class="mdcui-heading-toggle"');
880
+ expect(html).not.toContain("inline > <span class=\"mdcui-heading-toggle\"");
881
+ });
882
+
883
+ test("WUI heading sections ignore body-like text inside opaque elements", () => {
884
+ const html = wrapWuiHeadingSections(
885
+ '<!doctype html><html><head><script>'
886
+ + 'const opening = "<body>"; const closing = "</body>";'
887
+ + "</script></head><body>"
888
+ + '<h2 id="real">Real</h2><p>Body</p>'
889
+ + "</body></html>",
890
+ );
891
+
892
+ expect(html).toContain(
893
+ '<script>const opening = "<body>"; const closing = "</body>";</script>',
894
+ );
895
+ expect(html.match(/<section>/g)).toHaveLength(1);
896
+ expect(html.indexOf("</section>")).toBeLessThan(html.lastIndexOf("</body>"));
897
+ });
898
+
899
+ test("WUI heading toggles preserve inline code and opaque block markers", () => {
900
+ const inlineCode = wrapWuiHeadingSections(
901
+ '<h2 id="code"><code>foo</code></h2><p>Body</p>',
902
+ );
903
+ expect(inlineCode).toContain(
904
+ '<code><span class="mdcui-heading-toggle" role="button" tabindex="0" aria-expanded="true">f</span>oo</code>',
905
+ );
906
+ expect(inlineCode).not.toContain("MDCUI_HEADING_OPAQUE_");
907
+
908
+ const protectedBlock = wrapWuiHeadingSections(
909
+ '<pre><code><h2 id="fake">Fake</h2></code></pre><h2 id="real">Real</h2>',
910
+ );
911
+ expect(protectedBlock).toContain('<pre><code><h2 id="fake">Fake</h2></code></pre>');
912
+ expect(protectedBlock).not.toContain("MDCUI_HEADING_OPAQUE_");
913
+ expect(protectedBlock.match(/<section>/g)).toHaveLength(1);
914
+
915
+ const rawPreHeading = wrapWuiHeadingSections(
916
+ '<pre><h2 id="inside-pre">Inside pre</h2></pre>',
917
+ );
918
+ expect(rawPreHeading).toContain(
919
+ '<h2 id="inside-pre"><span class="mdcui-heading-toggle" '
920
+ + 'role="button" tabindex="0" aria-expanded="true">I</span>nside pre</h2>',
921
+ );
922
+ expect(rawPreHeading.match(/<section>/g)).toHaveLength(1);
923
+
924
+ const standaloneCode = wrapWuiHeadingSections(
925
+ '<CODE data-note="standalone > code"><h2 id="fake">Fake</h2></CODE>'
926
+ + '<h2 id="real">Real</h2><p>Body</p>',
927
+ );
928
+ expect(standaloneCode).toContain(
929
+ '<CODE data-note="standalone > code"><h2 id="fake">Fake</h2></CODE>',
930
+ );
931
+ expect(standaloneCode).not.toContain(
932
+ '<h2 id="fake"><span class="mdcui-heading-toggle"',
933
+ );
934
+ expect(standaloneCode.match(/<section>/g)).toHaveLength(1);
935
+ expect(standaloneCode).toContain(
936
+ '<h2 id="real"><span class="mdcui-heading-toggle"',
937
+ );
938
+
939
+ const quotedScript = wrapWuiHeadingSections(
940
+ '<script data-note="> </script>"><h2 id="fake">Fake</h2></script>'
941
+ + '<h2 id="real">Real</h2>',
942
+ );
943
+ expect(quotedScript).toContain(
944
+ '<script data-note="> </script>"><h2 id="fake">Fake</h2></script>',
945
+ );
946
+ expect(quotedScript.match(/<section>/g)).toHaveLength(1);
947
+
948
+ const nestedTemplate = wrapWuiHeadingSections(
949
+ '<template><template>Inner</template><h2 id="fake">Fake</h2></template>'
950
+ + '<h2 id="real">Real</h2>',
951
+ );
952
+ expect(nestedTemplate).toContain(
953
+ '<template><template>Inner</template><h2 id="fake">Fake</h2></template>',
954
+ );
955
+ expect(nestedTemplate.match(/<section>/g)).toHaveLength(1);
956
+
957
+ for (const tag of ["template", "title", "textarea", "script", "style"]) {
958
+ const foreignOpaque = wrapWuiHeadingSections(
959
+ `<svg><${tag}><h2 id="foreign">Foreign</h2></${tag}></svg>`
960
+ + '<h2 id="real">Real</h2>',
961
+ );
962
+ expect(foreignOpaque).toContain(
963
+ `<${tag}><section>\n<h2 id="foreign">`
964
+ + '<span class="mdcui-heading-toggle"',
965
+ );
966
+ expect(foreignOpaque.match(/<section>/g)).toHaveLength(2);
967
+ }
968
+
969
+ const htmlIntegrationTemplate = wrapWuiHeadingSections(
970
+ '<svg><foreignObject><template><h2 id="inert">Inert</h2></template>'
971
+ + '</foreignObject></svg><h2 id="real">Real</h2>',
972
+ );
973
+ expect(htmlIntegrationTemplate).toContain(
974
+ '<template><h2 id="inert">Inert</h2></template>',
975
+ );
976
+ expect(htmlIntegrationTemplate).not.toContain(
977
+ '<h2 id="inert"><span class="mdcui-heading-toggle"',
978
+ );
979
+ expect(htmlIntegrationTemplate.match(/<section>/g)).toHaveLength(1);
980
+
981
+ const unclosedScript = wrapWuiHeadingSections(
982
+ '<script><h2 id="fake">Fake</h2>',
983
+ );
984
+ expect(unclosedScript).toBe('<script><h2 id="fake">Fake</h2>');
985
+ expect(unclosedScript).not.toContain("<section>");
986
+
987
+ const rawLessThan = wrapWuiHeadingSections(
988
+ 'One < two <h2 id="real">Real</h2>',
989
+ );
990
+ expect(rawLessThan.match(/<section>/g)).toHaveLength(1);
991
+ });
992
+
993
+ test("WUI heading toggles wrap complete grapheme clusters", () => {
994
+ const graphemes = [
995
+ "e\u0301",
996
+ "👨‍👩‍👧‍👦",
997
+ "👍🏽",
998
+ "🇹🇼",
999
+ ];
1000
+
1001
+ for (const [index, grapheme] of graphemes.entries()) {
1002
+ const html = wrapWuiHeadingSections(
1003
+ `<h2 id="grapheme-${index}"><code>${grapheme} rest</code></h2>`,
1004
+ );
1005
+ expect(html).toContain(
1006
+ '<span class="mdcui-heading-toggle" role="button" tabindex="0" '
1007
+ + `aria-expanded="true">${grapheme}</span> rest`,
1008
+ );
1009
+ }
1010
+ });
1011
+
1012
+ test("WUI runtime decoration wraps a complete grapheme cluster", async () => {
1013
+ const graphemes = [
1014
+ "e\u0301",
1015
+ "👨‍👩‍👧‍👦",
1016
+ "👍🏽",
1017
+ "🇹🇼",
1018
+ ];
1019
+
1020
+ for (const [caseIndex, grapheme] of graphemes.entries()) {
1021
+ const textNode = {
1022
+ nodeType: 3,
1023
+ textContent: ` ${grapheme} heading`,
1024
+ };
1025
+ let rangeStart = -1;
1026
+ let rangeEnd = -1;
1027
+ let wrapped = "";
1028
+ let toggle;
1029
+ const heading = {
1030
+ tagName: "H2",
1031
+ id: `dynamic-${caseIndex}`,
1032
+ childNodes: [textNode],
1033
+ ownerDocument: null,
1034
+ querySelector() { return null; },
1035
+ };
1036
+ const documentObject = {
1037
+ readyState: "complete",
1038
+ addEventListener() {},
1039
+ querySelector() { return null; },
1040
+ querySelectorAll(selector) {
1041
+ return selector === "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]"
1042
+ ? [heading]
1043
+ : [];
1044
+ },
1045
+ createRange() {
1046
+ return {
1047
+ setStart(node, index) {
1048
+ expect(node).toBe(textNode);
1049
+ rangeStart = index;
1050
+ },
1051
+ setEnd(node, index) {
1052
+ expect(node).toBe(textNode);
1053
+ rangeEnd = index;
1054
+ },
1055
+ surroundContents(node) {
1056
+ toggle = node;
1057
+ wrapped = textNode.textContent.slice(rangeStart, rangeEnd);
1058
+ },
1059
+ };
1060
+ },
1061
+ createElement(tagName) {
1062
+ return {
1063
+ tagName: String(tagName).toUpperCase(),
1064
+ style: {},
1065
+ attributes: new Map(),
1066
+ setAttribute(name, value) { this.attributes.set(name, value); },
1067
+ };
1068
+ },
1069
+ };
1070
+ heading.ownerDocument = documentObject;
1071
+
1072
+ installWebDollar({
1073
+ document: documentObject,
1074
+ addEventListener() {},
1075
+ });
1076
+ await Promise.resolve();
1077
+
1078
+ expect(rangeStart).toBe(2);
1079
+ expect(rangeEnd).toBe(2 + grapheme.length);
1080
+ expect(wrapped).toBe(grapheme);
1081
+ expect(toggle.className).toBe("mdcui-heading-toggle");
1082
+ }
1083
+ });
1084
+
1085
+ test("TUI keydown.prevent runs before editing and blocks text input", async () => {
1086
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-keydown-"));
1087
+ const markdownPath = join(dir, "app.md");
1088
+ const markdown = [
1089
+ '```text#field @keydown.prevent="record(this,event)"',
1090
+ "a",
1091
+ "```",
1092
+ "",
1093
+ "```text#status",
1094
+ "waiting",
1095
+ "```",
1096
+ "",
1097
+ "```js front",
1098
+ "export function record(target, event) {",
1099
+ " const data = JSON.parse(JSON.stringify(event));",
1100
+ " $('#status').val(`${data.key}:${data.target.id}:${data.target.value}:${data.defaultPrevented}:${Object.keys(event).includes('toJSON')}:${target === event.target}`);",
1101
+ "}",
1102
+ "```",
1103
+ "",
1104
+ ].join("\n");
1105
+ let output = "";
1106
+ let proc;
1107
+ let terminal;
1108
+ try {
1109
+ await writeFile(markdownPath, markdown);
1110
+ terminal = new Bun.Terminal({
1111
+ cols: 60,
1112
+ rows: 16,
1113
+ data(_terminal, data) {
1114
+ output += Buffer.from(data).toString();
1115
+ },
1116
+ });
1117
+ proc = Bun.spawn({
1118
+ cmd: [bunBin, tui, "+2:4", markdownPath],
1119
+ cwd: dir,
1120
+ terminal,
1121
+ env: { ...process.env, TERM: "xterm-256color", COLUMNS: "60", LINES: "16" },
1122
+ });
1123
+ await waitFor(() => Bun.stripANSI(output).includes("waiting"));
1124
+ terminal.write("x");
1125
+ await waitFor(() => Bun.stripANSI(output).includes("x:field:a:true:false:true"));
1126
+ terminal.write("\x11");
1127
+ await Promise.race([proc.exited, Bun.sleep(2000)]);
1128
+ expect(Bun.stripANSI(output)).toContain("x:field:a:true:false:true");
1129
+ } finally {
1130
+ if (proc && proc.exitCode == null) proc.kill();
1131
+ terminal?.close();
1132
+ await rm(dir, { recursive: true, force: true });
1133
+ }
1134
+ }, 10000);
1135
+
1136
+ test("TUI input runs after insertion with the updated value", async () => {
1137
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-input-"));
1138
+ const markdownPath = join(dir, "app.md");
1139
+ const markdown = [
1140
+ '```text#field @input="changed(event)"',
1141
+ "a",
1142
+ "```",
1143
+ "",
1144
+ "```text#status",
1145
+ "waiting",
1146
+ "```",
1147
+ "",
1148
+ "```js front",
1149
+ "export function changed(event) {",
1150
+ " $('#status').val(`input:<${event.target.value}>`);",
1151
+ "}",
1152
+ "```",
1153
+ "",
1154
+ ].join("\n");
1155
+ let output = "";
1156
+ let proc;
1157
+ let terminal;
1158
+ try {
1159
+ await writeFile(markdownPath, markdown);
1160
+ terminal = new Bun.Terminal({
1161
+ cols: 60,
1162
+ rows: 16,
1163
+ data(_terminal, data) {
1164
+ output += Buffer.from(data).toString();
1165
+ },
1166
+ });
1167
+ proc = Bun.spawn({
1168
+ cmd: [bunBin, tui, "+2:4", markdownPath],
1169
+ cwd: dir,
1170
+ terminal,
1171
+ env: { ...process.env, TERM: "xterm-256color", COLUMNS: "60", LINES: "16" },
1172
+ });
1173
+ await waitFor(() => Bun.stripANSI(output).includes("waiting"));
1174
+ terminal.write("x");
1175
+ await waitFor(() => Bun.stripANSI(output).includes("input:<ax>"));
1176
+ terminal.write("\x11");
1177
+ await Promise.race([proc.exited, Bun.sleep(2000)]);
1178
+ expect(Bun.stripANSI(output)).toContain("input:<ax>");
1179
+ } finally {
1180
+ if (proc && proc.exitCode == null) proc.kill();
1181
+ terminal?.close();
1182
+ await rm(dir, { recursive: true, force: true });
1183
+ }
1184
+ }, 10000);
1185
+
1186
+ test("TUI onMdcuiLoad runs after startup resources are ready", async () => {
1187
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-load-"));
1188
+ const markdownPath = join(dir, "app.md");
1189
+ const markdown = [
1190
+ "# Ready",
1191
+ "",
1192
+ "```text#status",
1193
+ "waiting",
1194
+ "```",
1195
+ "",
1196
+ "```js front",
1197
+ "export function onMdcuiLoad() {",
1198
+ " $('#status').val('loaded');",
1199
+ "}",
1200
+ "```",
1201
+ "",
1202
+ ].join("\n");
1203
+ let output = "";
1204
+ let proc;
1205
+ let terminal;
1206
+ try {
1207
+ await writeFile(markdownPath, markdown);
1208
+ terminal = new Bun.Terminal({
1209
+ cols: 60,
1210
+ rows: 12,
1211
+ data(_terminal, data) {
1212
+ output += Buffer.from(data).toString();
1213
+ },
1214
+ });
1215
+ proc = Bun.spawn({
1216
+ cmd: [bunBin, tui, markdownPath],
1217
+ cwd: dir,
1218
+ terminal,
1219
+ env: { ...process.env, TERM: "xterm-256color", COLUMNS: "60", LINES: "12" },
1220
+ });
1221
+ await waitFor(() => Bun.stripANSI(output).includes("loaded"));
1222
+ terminal.write("\x11");
1223
+ await Promise.race([proc.exited, Bun.sleep(2000)]);
1224
+ expect(Bun.stripANSI(output)).toContain("loaded");
1225
+ } finally {
1226
+ if (proc && proc.exitCode == null) proc.kill();
1227
+ terminal?.close();
1228
+ await rm(dir, { recursive: true, force: true });
1229
+ }
1230
+ }, 10000);
1231
+
1232
+ test("TUI keydown alert releases and restores terminal input", async () => {
1233
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-keydown-alert-"));
1234
+ const markdownPath = join(dir, "app.md");
1235
+ const markdown = [
1236
+ '```text#field @keydown.prevent="celebrate(event)"',
1237
+ "focus here",
1238
+ "```",
1239
+ "",
1240
+ "```text#status",
1241
+ "waiting",
1242
+ "```",
1243
+ "",
1244
+ "```js front",
1245
+ "export function celebrate(event) {",
1246
+ " if (event.key === 'x') {",
1247
+ " alert('KEYDOWN ALERT');",
1248
+ " $('#status').val('alert closed');",
1249
+ " return;",
1250
+ " }",
1251
+ " $('#status').val(`second input: ${event.key}`);",
1252
+ "}",
1253
+ "```",
1254
+ "",
1255
+ ].join("\n");
1256
+ let output = "";
1257
+ let proc;
1258
+ let terminal;
1259
+ try {
1260
+ await writeFile(markdownPath, markdown);
1261
+ terminal = new Bun.Terminal({
1262
+ cols: 60,
1263
+ rows: 16,
1264
+ data(_terminal, data) {
1265
+ output += Buffer.from(data).toString();
1266
+ },
1267
+ });
1268
+ proc = Bun.spawn({
1269
+ cmd: [bunBin, tui, "+2:4", markdownPath],
1270
+ cwd: dir,
1271
+ terminal,
1272
+ env: { ...process.env, TERM: "xterm-256color", COLUMNS: "60", LINES: "16" },
1273
+ });
1274
+ await waitFor(() => Bun.stripANSI(output).includes("waiting"));
1275
+ terminal.write("x");
1276
+ await waitFor(() => Bun.stripANSI(output).includes("KEYDOWN ALERT"));
1277
+ terminal.write("\r");
1278
+ await waitFor(() => Bun.stripANSI(output).includes("alert closed"));
1279
+ terminal.write("z");
1280
+ await waitFor(() => Bun.stripANSI(output).includes("second input:z"));
1281
+ terminal.write("\x11");
1282
+ const exitCode = await Promise.race([
1283
+ proc.exited,
1284
+ Bun.sleep(2000).then(() => null),
1285
+ ]);
1286
+ expect(exitCode).toBe(0);
1287
+ expect(Bun.stripANSI(output)).toContain("second input:z");
1288
+ } finally {
1289
+ if (proc && proc.exitCode == null) proc.kill();
1290
+ terminal?.close();
1291
+ await rm(dir, { recursive: true, force: true });
1292
+ }
1293
+ }, 10000);