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
@@ -8,9 +8,7 @@ export class Highlighter {
8
8
  highlightLine(line, state = null, progress = null) {
9
9
  const text = String(line);
10
10
  const changes = new Map([[0, state?.group ?? "default"]]);
11
- const nextState = state
12
- ? scanRegion(changes, text, 0, state, this.rules, progress)
13
- : scanTop(changes, text, 0, this.rules, progress);
11
+ const nextState = scanLine(changes, text, state, this.rules, progress);
14
12
  return {
15
13
  changes: normalizeChanges(changes, text.length),
16
14
  state: nextState,
@@ -29,16 +27,18 @@ export class Highlighter {
29
27
  }
30
28
  }
31
29
 
32
- export function resolveRules(rules, definitions, seen) {
30
+ export function resolveRules(rules, definitions, seen, ancestors = new Set()) {
31
+ if (ancestors.has(rules) || ancestors.size >= 128) return [];
32
+ const path = new Set(ancestors); path.add(rules);
33
33
  const out = [];
34
34
  for (const rule of rules) {
35
35
  if (rule.type === "include") {
36
36
  if (seen.has(rule.include)) continue;
37
37
  seen.add(rule.include);
38
38
  const def = definitions.find((candidate) => candidate.filetype === rule.include);
39
- if (def) out.push(...resolveRules(def.rules, definitions, seen));
39
+ if (def) out.push(...resolveRules(def.rules, definitions, seen, path));
40
40
  } else if (rule.type === "region") {
41
- out.push({ ...rule, rules: resolveRules(rule.rules ?? [], definitions, new Set(seen)) });
41
+ out.push({ ...rule, rules: resolveRules(rule.rules ?? [], definitions, new Set(seen), path) });
42
42
  } else {
43
43
  out.push(rule);
44
44
  }
@@ -53,67 +53,67 @@ export function flattenRules(rules, definitions, seen) {
53
53
  });
54
54
  }
55
55
 
56
- function scanTop(changes, line, pos, rules, progress = null) {
57
- let cursor = pos;
56
+ // Iterative state machine: adjacent tokens on a minified line must not consume
57
+ // call-stack depth. A state repeated at an unchanged position steps forward.
58
+ function scanLine(changes, line, region, rootRules, progress) {
59
+ let cursor = 0, previous = -1;
60
+ const visited = new Set();
61
+ const parents = new Set();
62
+ for (let r = region; r; r = r.parent) {
63
+ if (parents.has(r) || parents.size >= 128) { region = null; changes.set(0, "default"); break; }
64
+ parents.add(r);
65
+ }
58
66
  while (cursor <= line.length) {
59
- progress?.(cursor);
60
- const regionStart = findFirstRegion(rules, line, cursor, progress);
61
- if (!regionStart) {
62
- applyPatterns(changes, line, cursor, line.length, rules, "default", progress);
63
- changes.set(line.length, "default");
64
- progress?.(line.length);
67
+ if (cursor !== previous) { visited.clear(); previous = cursor; }
68
+ if (visited.has(region)) {
69
+ // A zero-width child can end and reopen forever at the same position.
70
+ // Skip one character in the current region, like a zero-width match.
71
+ cursor = advanceIndex(line, cursor);
72
+ continue;
73
+ }
74
+ if (visited.size >= 256) {
75
+ // Fresh object states from malformed externally supplied trees.
76
+ // Discard this line's unfinished region; do not poison subsequent lines.
77
+ changes.set(cursor, "default"); changes.set(line.length, "default");
65
78
  return null;
66
79
  }
67
-
68
- applyPatterns(changes, line, cursor, regionStart.start, rules, "default", progress);
69
- changes.set(regionStart.start, regionStart.region.limitGroup ?? "default");
70
- changes.set(regionStart.end, regionStart.region.group);
71
- progress?.(regionStart.end);
72
-
73
- const state = scanRegion(changes, line, regionStart.end, regionStart.region, rules, progress);
74
- if (state) return state;
75
- return null;
76
- }
77
- return null;
78
- }
79
-
80
- function scanRegion(changes, line, pos, region, rootRules, progress = null) {
81
- let cursor = pos;
82
- const rules = region.rules ?? [];
83
- while (cursor <= line.length) {
80
+ visited.add(region);
84
81
  progress?.(cursor);
85
- const end = findMatch(region.end, line, cursor, region.skip, progress);
82
+ const rules = region?.rules ?? rootRules;
83
+ const end = region ? findMatch(region.end, line, cursor, region.skip, progress) : null;
86
84
  const nested = findFirstRegion(rules, line, cursor, progress);
87
-
88
85
  if (end && (!nested || end.start <= nested.start)) {
89
86
  applyPatterns(changes, line, cursor, end.start, rules, region.group, progress);
90
87
  changes.set(end.start, region.limitGroup ?? region.group);
91
88
  changes.set(end.end, parentGroup(region));
92
89
  progress?.(end.end);
93
- if (region.parent) return scanRegion(changes, line, end.end, region.parent, rootRules, progress);
94
- return scanTop(changes, line, Math.max(end.end, end.start + 1), rootRules, progress);
90
+ const parent = region.parent ?? null;
91
+ cursor = parent ? end.end : Math.max(end.end, advanceIndex(line, end.start));
92
+ region = parent;
93
+ continue;
95
94
  }
96
-
97
95
  if (nested) {
98
- applyPatterns(changes, line, cursor, nested.start, rules, region.group, progress);
99
- changes.set(nested.start, nested.region.limitGroup ?? region.group);
100
- const child = { ...nested.region, parent: region };
101
- changes.set(nested.end, child.group);
96
+ applyPatterns(changes, line, cursor, nested.start, rules, region?.group ?? "default", progress);
97
+ changes.set(nested.start, nested.region.limitGroup ?? nested.region.group);
98
+ region = region ? { ...nested.region, parent: region } : nested.region;
99
+ changes.set(nested.end, region.group);
102
100
  progress?.(nested.end);
103
- const state = scanRegion(changes, line, nested.end, child, rootRules, progress);
104
- if (state) return state;
105
- cursor = Math.max(nested.end + 1, nextChangeAfter(changes, nested.end));
101
+ cursor = nested.end;
106
102
  continue;
107
103
  }
108
-
109
- applyPatterns(changes, line, cursor, line.length, rules, region.group, progress);
110
- changes.set(line.length, region.group);
104
+ applyPatterns(changes, line, cursor, line.length, rules, region?.group ?? "default", progress);
105
+ changes.set(line.length, region?.group ?? "default");
111
106
  progress?.(line.length);
112
107
  return region;
113
108
  }
114
109
  return region;
115
110
  }
116
111
 
112
+ // AdvanceStringIndex for Unicode regexps, including isolated surrogate units.
113
+ function advanceIndex(text, index) {
114
+ return index + (text.codePointAt(index) > 0xFFFF ? 2 : 1);
115
+ }
116
+
117
117
  function applyPatterns(changes, line, start, end, rules, fallbackGroup, progress = null) {
118
118
  if (end < start) return;
119
119
  const length = end - start;
@@ -161,18 +161,12 @@ function findMatch(regex, line, pos, skip, progress = null) {
161
161
  const global = globalize(regex);
162
162
  global.lastIndex = pos;
163
163
  let match;
164
- if (!progress) {
165
- match = global.exec(masked);
166
- if (!match) return null;
167
- const start = match.index ?? 0;
168
- return { start, end: start + match[0].length };
169
- }
170
164
  while ((match = global.exec(masked)) !== null) {
171
165
  const start = match.index ?? 0;
172
166
  const end = start + match[0].length;
173
167
  progress?.(start);
174
168
  if (start >= pos) return { start, end };
175
- if (match[0].length === 0) global.lastIndex++;
169
+ global.lastIndex = Math.max(global.lastIndex, advanceIndex(masked, start));
176
170
  }
177
171
  progress?.(line.length);
178
172
  return null;
@@ -189,7 +183,7 @@ function findAllMatches(regex, line, start, end, progress = null) {
189
183
  if (matchStart >= end) break;
190
184
  const matchEnd = matchStart + match[0].length;
191
185
  if (match[0].length === 0) {
192
- global.lastIndex++;
186
+ global.lastIndex = advanceIndex(line, matchStart);
193
187
  continue;
194
188
  }
195
189
  if (matchEnd > start) out.push({ start: Math.max(matchStart, start), end: Math.min(matchEnd, end) });
@@ -202,7 +196,7 @@ function findAllMatches(regex, line, start, end, progress = null) {
202
196
  if (matchStart >= end) break;
203
197
  progress?.(matchStart);
204
198
  if (match[0].length === 0) {
205
- global.lastIndex++;
199
+ global.lastIndex = advanceIndex(line, matchStart);
206
200
  continue;
207
201
  }
208
202
  if (matchEnd <= start || matchStart >= end) continue;
@@ -229,13 +223,6 @@ function parentGroup(region) {
229
223
  return region.parent?.group ?? "default";
230
224
  }
231
225
 
232
- function nextChangeAfter(changes, index) {
233
- for (const key of [...changes.keys()].sort((a, b) => a - b)) {
234
- if (key > index) return key;
235
- }
236
- return index + 1;
237
- }
238
-
239
226
  function globalize(regex) {
240
227
  const flags = new Set(regex.flags.split(""));
241
228
  flags.add("g");
@@ -1,4 +1,5 @@
1
1
  import { basename } from "node:path";
2
+ import { convertGoRegexp } from "./go-regexp.js";
2
3
 
3
4
  export class SyntaxHeader {
4
5
  constructor({ filetype = "", filename = "", header = "", signature = "" } = {}) {
@@ -195,15 +196,17 @@ function parseRule(rule) {
195
196
 
196
197
  function compileRegex(pattern) {
197
198
  if (!pattern) return null;
198
- const translated = translateGoRegexp(pattern);
199
199
  try {
200
- return new RegExp(translated, "u");
201
- } catch {
200
+ // Conversion may reject an excessively nested rule. Keep that failure
201
+ // local to this regexp rather than dropping the entire syntax definition.
202
+ const translated = convertGoRegexp(translateGoRegexp(pattern));
202
203
  try {
203
- return new RegExp(translated);
204
+ return new RegExp(translated, "u");
204
205
  } catch {
205
- return null;
206
+ return new RegExp(translated);
206
207
  }
208
+ } catch {
209
+ return null;
207
210
  }
208
211
  }
209
212
 
@@ -0,0 +1,98 @@
1
+ import {test, expect} from 'bun:test';
2
+ import {convertGoRegexp} from '../src/highlight/go-regexp.js';
3
+ import {SyntaxDefinition,parseHeaderYaml} from '../src/highlight/parser.js';
4
+ const regex = s => new RegExp(convertGoRegexp(s), 'u');
5
+ test('Go flags apply to the remaining alternatives, preserving precedence', () => {
6
+ const r = regex('^a(?i)b$|^c$');
7
+ expect(r.test('aB')).toBe(true);
8
+ expect(r.test('AB')).toBe(false);
9
+ expect(r.test('C')).toBe(true);
10
+ expect(r.test('aC')).toBe(false);
11
+ });
12
+ test('flag changes stay inside groups and can be disabled', () => {
13
+ const r = regex('^(a(?i)b|c)d$');
14
+ expect(r.test('aBd')).toBe(true);
15
+ expect(r.test('Cd')).toBe(true);
16
+ expect(r.test('CD')).toBe(false);
17
+ expect(regex('^(?i)a(?-i)b$').test('Ab')).toBe(true);
18
+ expect(regex('^(?i)a(?-i)b$').test('AB')).toBe(false);
19
+ expect(regex('^(?i:a(?-i:b))c$').test('Abc')).toBe(true);
20
+ expect(regex('^(?i:a(?-i:b))c$').test('ABC')).toBe(false);
21
+ });
22
+ test('capture numbers, scoped flags, escaped literals and class brackets survive', () => {
23
+ expect(regex('^(?i)(a)(b)$').exec('AB').slice(1)).toEqual(['A','B']);
24
+ expect(regex('(?i:a)+').test('AAA')).toBe(true);
25
+ expect(regex('\\(\\?i\\)').test('(?i)')).toBe(true);
26
+ const url = regex('(https?|ftp|ssh)://\\S*[^])>\\s,.]');
27
+ expect(url.exec('https://example.com)')[0]).toBe('https://example.com');
28
+ expect(regex('(?s)^a.b$').test('a\nb')).toBe(true);
29
+ expect(regex('(?m)^b$').test('a\nb\nc')).toBe(true);
30
+ expect(convertGoRegexp('\\bword\\b')).toBe('\\bword\\b');
31
+ });
32
+ test('all bundled syntax regexes compile, and Dockerfile keywords match', async () => {
33
+ const root = new URL('../runtime/syntax/', import.meta.url).pathname;
34
+ const failures = [];
35
+ function walk(rules, file) {
36
+ for (const r of rules) {
37
+ if (r.type === 'pattern' && !r.regex) failures.push(file+': '+r.source);
38
+ if (r.type === 'region') {
39
+ for (const k of ['start','end','skip']) if (r.source[k] && !r[k]) failures.push(file+': '+r.source[k]);
40
+ walk(r.rules,file);
41
+ }
42
+ }
43
+ }
44
+ for await (const file of new Bun.Glob('*.yaml').scan(root)) {
45
+ const source = Bun.YAML.parse(await Bun.file(root+'/'+file).text());
46
+ const header = parseHeaderYaml(source);
47
+ for (const [key,re] of [['filename','fileNameRegex'],['header','headerRegex'],['signature','signatureRegex']])
48
+ if (header[key] && !header[re]) failures.push(file+': '+key);
49
+ const def = new SyntaxDefinition(header,source);
50
+ walk(def.rules,file);
51
+ if (file === 'dockerfile.yaml') {
52
+ expect(header.matchFileName('/tmp/Dockerfile')).toBe(true);
53
+ const keyword = def.rules.find(r=>r.group==='type.keyword').regex;
54
+ for (const line of ['FROM debian','run echo hi','COPY . /app']) expect(keyword.test(line)).toBe(true);
55
+ expect(keyword.test('FROMAGE debian')).toBe(false);
56
+ }
57
+ }
58
+ expect(failures).toEqual([]);
59
+ });
60
+ test('mixed and negated POSIX classes retain ASCII Go semantics',()=>{
61
+ expect(regex('^[[:alnum:].%_+-]+@[[:alnum:].-]+$').test('a.b+tag@example.com')).toBe(true);
62
+ expect(regex('^[^[:space:]]+:$').test('Subject:')).toBe(true);
63
+ expect(regex('^[^[:space:]]+:$').test('Bad Header:')).toBe(false);
64
+ expect(regex('^[{[:space:]]-block([[:space:]{}]|$)').test(' -block ')).toBe(true);
65
+ expect(regex('^[x[:space:]]$').test('\t')).toBe(true);
66
+ expect(regex('^[x[:space:]]$').test('\u00a0')).toBe(false);
67
+ expect(regex('^[x[:punct:]]$').test('[')).toBe(true);
68
+ expect(regex('^[x[:punct:]]$').test('A')).toBe(false);
69
+ });
70
+ test('modifiers are emitted only where flags change',()=>{
71
+ expect(convertGoRegexp('[]a]')).toBe('[\\]a]');
72
+ expect(convertGoRegexp('(?P<x>a)')).toBe('(?<x>a)');
73
+ expect(convertGoRegexp('[[:alpha:]_]')).toBe('[A-Za-z_]');
74
+ expect(convertGoRegexp('(?i)\\b(select|from)\\b')).toBe('(?i:\\b(select|from)\\b)');
75
+ expect(regex('^(?i)a(?-i:b)$').test('Ab')).toBe(true);
76
+ expect(regex('^(?i)a(?-i:b)$').test('AB')).toBe(false);
77
+ });
78
+
79
+ test('converter bounds nested groups without limiting adjacent groups',()=>{
80
+ expect(()=>convertGoRegexp('(?i)'+'('.repeat(50000)+'x'+')'.repeat(50000))).toThrow('nesting exceeds');
81
+ expect(regex('(?i)'+'('.repeat(256)+'x'+')'.repeat(256)).test('X')).toBe(true);
82
+ expect(()=>convertGoRegexp('(?i)'+'(x)'.repeat(50000))).not.toThrow();
83
+ });
84
+
85
+ test('conversion failure disables only its rule and preserves the loaded syntax',async()=>{
86
+ const {loadSyntaxDefinitions}=await import('../src/highlight/parser.js');
87
+ const {Highlighter}=await import('../src/highlight/highlighter.js');
88
+ const deep='(?i)'+'('.repeat(50000)+'x'+')'.repeat(50000);
89
+ const source={filetype:'guarded',detect:{filename:'\\.guarded$'},rules:[{bad:deep},{keyword:'(?i)hello'},{invalid:'['}]};
90
+ const def=new SyntaxDefinition(parseHeaderYaml(source),source);
91
+ expect(def.rules[0].regex).toBeNull();expect(def.rules[2].regex).toBeNull();
92
+ expect(def.rules[1].regex.test('HELLO')).toBe(true);
93
+ const runtime={list:type=>type===1?[{name:'guarded',text:async()=>JSON.stringify(source)}]:[]};
94
+ const defs=await loadSyntaxDefinitions(runtime);
95
+ expect(defs.map(d=>d.filetype)).toEqual(['guarded']);
96
+ expect(defs[0].header.matchFileName('example.guarded')).toBe(true);
97
+ expect(new Highlighter(defs[0],defs).highlightLine('HELLO').changes.get(0)).toBe('keyword');
98
+ });
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test";
2
+ import { escapesSlashTags } from "./markdown-behavior.mjs";
2
3
  import { mkdtemp, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
@@ -347,23 +348,27 @@ test("raw heading markers do not change quoted attribute parsing", () => {
347
348
  expect(rendered.html).toContain('<h1 id="中文-1">中文</h1>');
348
349
  });
349
350
 
350
- test("raw heading markers preserve slash-separated HTML attributes", () => {
351
- const markdown = `<h1/id=中文>Raw</h1>
352
-
353
- # 中文
354
- `;
355
- expect(collectRawHtmlHeadingDeclarations(markdown)).toEqual([
356
- {
357
- id: "中文",
358
- kind: "raw HTML heading",
359
- line: 1,
360
- source: "<h1/id=中文>Raw</h1>",
361
- },
351
+ test("raw heading markers follow Bun's slash-separated HTML behavior", () => {
352
+ const markdown = "<h1/id=中文>Raw</h1>\n\n# 中文\n";
353
+ expect(collectRawHtmlHeadingDeclarations(markdown)).toEqual(escapesSlashTags ? [] : [
354
+ { id: "中文", kind: "raw HTML heading", line: 1, source: "<h1/id=中文>Raw</h1>" },
362
355
  ]);
363
356
  const rendered = renderMarkdownWithHeadingIds(markdown);
364
- expect(rendered.headings.map(heading => heading.id)).toEqual(["中文-1"]);
365
- expect(rendered.html).toContain("<h1/id=中文>Raw</h1>");
366
- expect(rendered.html).toContain('<h1 id="中文-1">中文</h1>');
357
+ const id = escapesSlashTags ? "中文" : "中文-1";
358
+ expect(rendered.normalized).toBe(true);
359
+ expect(rendered.headings.map(heading => heading.id)).toEqual([id]);
360
+ expect(rendered.html).toContain(escapesSlashTags
361
+ ? "<p>&lt;h1/id=中文&gt;Raw</h1></p>"
362
+ : "<h1/id=中文>Raw</h1>");
363
+ expect(rendered.html).toContain(`<h1 id="${id}">中文</h1>`);
364
+ });
365
+
366
+ test("slash-tag ambiguity follows Bun's actual Markdown output", () => {
367
+ const rendered = renderMarkdownWithHeadingIds('<div/a< x=">TAIL\n\n# 中文\n');
368
+ expect(rendered.normalized).toBe(escapesSlashTags);
369
+ expect(rendered.headings.map(heading => heading.id)).toEqual(escapesSlashTags ? ["中文"] : []);
370
+ expect(rendered.html).toContain(escapesSlashTags
371
+ ? '<h1 id="中文">中文</h1>' : '<h1 id="">中文</h1>');
367
372
  });
368
373
 
369
374
  test("self-closing inline headings cannot suppress other raw collisions", () => {
@@ -511,7 +516,7 @@ test("quoted tag delimiters preserve raw IDs and Markdown heading metadata", ()
511
516
  });
512
517
 
513
518
  test("browser alignment failures never fall back to lexical heading rewrites", () => {
514
- const markdown = '<div/a< x=">TAIL\n\n# 中文\n';
519
+ const markdown = '<div x=">TAIL\n\n# 中文\n';
515
520
  const rendered = renderMarkdownWithHeadingIds(markdown);
516
521
  expect(rendered.normalized).toBe(false);
517
522
  expect(rendered.headings).toEqual([]);
@@ -0,0 +1,73 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {SyntaxDefinition,parseHeaderYaml} from '../src/highlight/parser.js';
3
+ import {Highlighter} from '../src/highlight/highlighter.js';
4
+ async function highlighter(name){
5
+ const source=Bun.YAML.parse(await Bun.file(new URL('../runtime/syntax/'+name+'.yaml',import.meta.url)).text());
6
+ return new Highlighter(new SyntaxDefinition(parseHeaderYaml(source),source));
7
+ }
8
+ function groupAt(result,index){let group='default';for(const [pos,value]of result.changes){if(pos>index)break;group=value;}return group;}
9
+ for(const [syntax,first]of [['html','<div class="hello">Hello</div>'],['xml','<Project Sdk="Microsoft.NET.Sdk">']]){
10
+ test(syntax+' nested attribute returns to outer tag and does not leak to next line',async()=>{
11
+ const h=await highlighter(syntax),r=h.highlightLine(first);
12
+ expect(r.state).toBeNull();
13
+ expect(groupAt(r,first.indexOf('>'))).toBe('symbol.tag');
14
+ const next=h.highlightLine('<p>Next line</p>',r.state);
15
+ expect(groupAt(next,3)).toBe('default');
16
+ expect(next.state).toBeNull();
17
+ expect([...r.changes.keys()].every(p=>p<=first.length)).toBe(true);
18
+ });
19
+ test(syntax+' retains an unfinished attribute across lines then exits it',async()=>{
20
+ const h=await highlighter(syntax);
21
+ const a=h.highlightLine('<div title="first');expect(a.state?.group).toBe('constant.string');
22
+ const b=h.highlightLine('second">Text</div>',a.state);
23
+ expect(groupAt(b,0)).toBe('constant.string');
24
+ expect(groupAt(b,8)).toBe('default');expect(b.state).toBeNull();
25
+ });
26
+ }
27
+ test('HTML multiple attributes, adjacent tags and nested escaped quotes',async()=>{
28
+ const h=await highlighter('html');
29
+ const line='<a href="/path" title="say \\"hi\\"">Link</a><b>next</b>';
30
+ const r=h.highlightLine(line);
31
+ expect(r.state).toBeNull();expect(groupAt(r,line.indexOf('Link'))).toBe('default');
32
+ expect(groupAt(r,line.indexOf('next'))).toBe('default');
33
+ });
34
+ test('zero-width child/parent loop steps forward and keeps highlighting the line',()=>{
35
+ const h=new Highlighter({rules:[{type:'region',group:'outer',start:/</u,end:/>/u,rules:[{type:'region',group:'inner',start:/(?=x)/u,end:/(?=x)/u,rules:[]}]},{type:'pattern',group:'keyword',regex:/END/u}]});
36
+ let calls=0;const line='<x>END';const r=h.highlightLine(line,null,()=>{if(++calls>100)throw Error('loop');});
37
+ expect(r.state).toBeNull();expect(calls).toBeLessThan(100);
38
+ expect(groupAt(r,1)).toBe('outer');expect(groupAt(r,line.indexOf('END'))).toBe('keyword');
39
+ });
40
+ test('empty pattern matches advance past full Unicode characters, with or without progress',()=>{
41
+ const h=new Highlighter({rules:[{type:'pattern',group:'empty',regex:/(?=.)/u},{type:'pattern',group:'number',regex:/42/u}]});
42
+ for(const progress of [null,()=>{}]){
43
+ const r=h.highlightLine('😀\0\ud80042',null,progress);
44
+ expect(groupAt(r,4)).toBe('number');expect(r.state).toBeNull();
45
+ }
46
+ });
47
+ test('NUL and isolated surrogate units inside HTML do not leak state',async()=>{
48
+ const h=await highlighter('html');
49
+ const r=h.highlightLine('<a title="\0😀\ud800">hello\0\udc00</a>');
50
+ expect(r.state).toBeNull();
51
+ expect(h.highlightLine('\0'.repeat(10000)).state).toBeNull();
52
+ });
53
+ test('long lines finish at their final token without recursion or truncation',()=>{
54
+ const h=new Highlighter({rules:[{type:'region',group:'string',start:/"/u,end:/"/u,rules:[]},{type:'pattern',group:'keyword',regex:/END/u}]});
55
+ const line='"x";'.repeat(50000)+'END';
56
+ const r=h.highlightLine(line);
57
+ expect(r.state).toBeNull();expect(groupAt(r,line.length-1)).toBe('keyword');
58
+ expect([...r.changes.keys()].at(-1)).toBe(line.length);
59
+ },30000);
60
+ test('cyclic incoming state and cyclic syntax trees cannot recurse forever',()=>{
61
+ const region={type:'region',group:'cycle',start:/</u,end:/>/u,rules:[]};region.rules.push(region);
62
+ const h=new Highlighter({rules:[region]});
63
+ expect(h.highlightLine('plain').state).toBeNull();
64
+ const state={group:'cycle',rules:[]};state.parent=state;
65
+ expect(h.highlightLine('plain',state).state).toBeNull();
66
+ });
67
+ test('externally supplied zero-width recursive state has a no-progress bound',()=>{
68
+ const child={type:'region',group:'inner',start:/(?=x)/u,end:/$/u,rules:[]};child.rules.push(child);
69
+ const state={group:'outer',end:/$/u,rules:[child]};
70
+ const h=new Highlighter({rules:[]});let calls=0;
71
+ const r=h.highlightLine('x',state,()=>{if(++calls>5000)throw Error('loop');});
72
+ expect(r.state).toBeNull();expect(calls).toBeLessThan(5000);
73
+ });
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test";
2
+ import { escapesSlashTags } from "./markdown-behavior.mjs";
2
3
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
@@ -39,7 +40,7 @@ test("--check exits 0 and reports unique IDs", async () => {
39
40
  });
40
41
 
41
42
  test("--check fails closed when browser heading parsing is ambiguous", async () => {
42
- const result = await runCheck('<div/a< x=">TAIL\n\n# 中文\n');
43
+ const result = await runCheck('<div x=">TAIL\n\n# 中文\n');
43
44
  expect(result.exitCode).toBe(2);
44
45
  expect(result.stderr.toString()).toContain(
45
46
  "Markdown headings could not be classified safely",
@@ -359,15 +360,37 @@ test("--check ignores heading-looking text inside raw attributes", async () => {
359
360
  expect(output).toContain("FAILED");
360
361
  });
361
362
 
362
- test("--check preserves slash-separated raw heading attributes", async () => {
363
+ test("--check follows Bun's slash-separated raw heading behavior", async () => {
363
364
  const result = await runCheck("<h1/id=中文>Raw</h1>\n\n# 中文\n");
364
365
  const output = Bun.stripANSI(result.stdout.toString());
365
- expect(result.exitCode).toBe(1);
366
- expect(output).toContain("ID #中文");
367
- expect(output).toContain("Type: raw HTML heading");
368
- expect(output).toContain("Type: heading");
369
- expect(output).toContain("FAILED");
370
- });
366
+ expect(result.exitCode).toBe(escapesSlashTags ? 0 : 1);
367
+ if (escapesSlashTags) {
368
+ expect(output).toContain("Selectable IDs: 1");
369
+ expect(output).toContain("PASSED");
370
+ expect(output).not.toContain("Type: raw HTML heading");
371
+ } else {
372
+ expect(output).toContain("ID #中文");
373
+ expect(output).toContain("Type: raw HTML heading");
374
+ expect(output).toContain("Type: heading");
375
+ expect(output).toContain("FAILED");
376
+ }
377
+ });
378
+
379
+ for (const [command, run] of [["--check", runCheck], ["--outline", runOutline]]) {
380
+ test(`${command} follows Bun's slash-tag ambiguity behavior`, async () => {
381
+ const result = await run('<div/a< x=">TAIL\n\n# 中文\n');
382
+ expect(result.exitCode).toBe(escapesSlashTags ? 0 : 2);
383
+ if (escapesSlashTags) {
384
+ expect(result.stderr.toString()).toBe("");
385
+ if (command === "--check") expect(result.stdout.toString()).toContain("PASSED");
386
+ else expect(result.stdout.toString()).toContain("中文");
387
+ } else {
388
+ expect(result.stderr.toString()).toContain("Markdown headings could not be classified safely");
389
+ expect(result.stdout.toString()).not.toContain("PASSED");
390
+ if (command === "--outline") expect(result.stdout.toString()).toBe("");
391
+ }
392
+ });
393
+ }
371
394
 
372
395
  test("--check ignores heading-like custom tag names", async () => {
373
396
  const result = await runCheck(
@@ -494,7 +517,7 @@ value
494
517
  });
495
518
 
496
519
  test("--outline fails closed when browser heading parsing is ambiguous", async () => {
497
- const result = await runOutline('<div/a< x=">TAIL\n\n# 中文\n');
520
+ const result = await runOutline('<div x=">TAIL\n\n# 中文\n');
498
521
  expect(result.exitCode).toBe(2);
499
522
  expect(result.stderr.toString()).toContain(
500
523
  "Markdown headings could not be classified safely",
@@ -0,0 +1,9 @@
1
+ // Probe the parser, not Bun.version: canaries and backports can differ.
2
+ // CommonMark allows `/` after a block tag name only as part of `/>`.
3
+ const probe = Bun.markdown.html("<h1/id=x>Raw</h1>\n");
4
+ const raw = "<h1/id=x>Raw</h1>\n";
5
+ const escaped = "<p>&lt;h1/id=x&gt;Raw</h1></p>\n";
6
+ if (probe !== raw && probe !== escaped) {
7
+ throw new Error(`Unexpected Bun Markdown slash-tag behavior: ${JSON.stringify(probe)}`);
8
+ }
9
+ export const escapesSlashTags = probe === escaped;
@@ -0,0 +1,46 @@
1
+ # Normal HTML follow-up
2
+
3
+ Tested with the same unmodified micro highlighter oracle and upstream YAML as
4
+ REPORT.md. See normal-html-report.json for counts and first differing positions.
5
+ The full comparison.html was also passed to both engines as input, unchanged.
6
+
7
+ The minimal reproduction is ordinary valid HTML:
8
+
9
+ ```html
10
+ <div class="hello">Hello</div>
11
+ <p>Next line</p>
12
+ ```
13
+
14
+ With html.yaml (the definition selected by the .html filename), jmi ends the
15
+ first line with a constant.string state and colors the entire second line as a
16
+ string. This is a real nested-region state bug, not just a malformed generated
17
+ sample. normal-html.html contains a complete document for manual reproduction.
18
+
19
+ For the generated comparison.html, html.yaml produced 731354 differing character
20
+ groups, including 438494 characters marked as strings only by jmi. This large
21
+ one-line file amplifies the state issue; it is not a typical per-file error rate.
22
+ The same full file matched with explicitly selected html4.yaml and html5.yaml,
23
+ which have different rules and are not the default .html filename match.
24
+
25
+ This check did not modify the highlighter or the syntax files.
26
+
27
+ ## After the nested-region fix
28
+
29
+ The historical reproduction above is now covered by highlight-regions.test.js.
30
+ The child scanner already resumes its parent and processes the rest of the line;
31
+ returning that result directly avoids rescanning closing quotes as new openings.
32
+ Both HTML and XML now return to the correct outer state, including when an
33
+ attribute legitimately spans multiple lines. Original syntax YAML is unchanged.
34
+
35
+ The current normal-html-report.json records the follow-up: simple HTML,
36
+ two-attribute HTML and a complete normal document have no extra string-colored
37
+ characters. Their remaining differences are local token groups, such as micro
38
+ using symbol.tag where jmi recognizes an attribute as identifier. The current
39
+ comparison.html still has local group differences and is not fully identical.
40
+ It is regenerated from the latest results, so its total size differs from the
41
+ historical input above; raw differing-character totals are not a fixed-corpus
42
+ before/after benchmark.
43
+
44
+ The 158-syntax corpus now has 130 identical samples and 28 with differences.
45
+ XML and MSBuild samples match completely. Mixed POSIX character classes are
46
+ also translated; default/email, mail and privoxy-action samples now match.