@fairyhunter13/opentui-core 0.1.90 → 0.1.92

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 (571) hide show
  1. package/3d/SpriteResourceManager.d.ts +74 -0
  2. package/3d/SpriteUtils.d.ts +13 -0
  3. package/3d/TextureUtils.d.ts +24 -0
  4. package/3d/ThreeRenderable.d.ts +40 -0
  5. package/3d/WGPURenderer.d.ts +61 -0
  6. package/3d/animation/ExplodingSpriteEffect.d.ts +71 -0
  7. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +76 -0
  8. package/3d/animation/SpriteAnimator.d.ts +124 -0
  9. package/3d/animation/SpriteParticleGenerator.d.ts +62 -0
  10. package/3d/canvas.d.ts +44 -0
  11. package/3d/index.d.ts +12 -0
  12. package/3d/physics/PlanckPhysicsAdapter.d.ts +19 -0
  13. package/3d/physics/RapierPhysicsAdapter.d.ts +19 -0
  14. package/3d/physics/physics-interface.d.ts +27 -0
  15. package/3d.d.ts +2 -0
  16. package/3d.js +34042 -0
  17. package/3d.js.map +155 -0
  18. package/LICENSE +21 -0
  19. package/NativeSpanFeed.d.ts +41 -0
  20. package/README.md +2 -2
  21. package/Renderable.d.ts +334 -0
  22. package/animation/Timeline.d.ts +126 -0
  23. package/ansi.d.ts +13 -0
  24. package/buffer.d.ts +107 -0
  25. package/console.d.ts +143 -0
  26. package/edit-buffer.d.ts +98 -0
  27. package/editor-view.d.ts +73 -0
  28. package/index-e6ec7apq.js +18415 -0
  29. package/index-e6ec7apq.js.map +64 -0
  30. package/index-h066zmrb.js +12619 -0
  31. package/index-h066zmrb.js.map +43 -0
  32. package/index-ynzawt3n.js +113 -0
  33. package/index-ynzawt3n.js.map +10 -0
  34. package/index.d.ts +21 -0
  35. package/index.js +430 -0
  36. package/index.js.map +9 -0
  37. package/lib/KeyHandler.d.ts +61 -0
  38. package/lib/RGBA.d.ts +25 -0
  39. package/lib/ascii.font.d.ts +508 -0
  40. package/lib/border.d.ts +49 -0
  41. package/lib/bunfs.d.ts +7 -0
  42. package/lib/clipboard.d.ts +17 -0
  43. package/lib/clock.d.ts +15 -0
  44. package/lib/data-paths.d.ts +26 -0
  45. package/lib/debounce.d.ts +42 -0
  46. package/lib/detect-links.d.ts +6 -0
  47. package/lib/env.d.ts +42 -0
  48. package/lib/extmarks-history.d.ts +17 -0
  49. package/lib/extmarks.d.ts +89 -0
  50. package/lib/hast-styled-text.d.ts +17 -0
  51. package/lib/index.d.ts +21 -0
  52. package/lib/keymapping.d.ts +25 -0
  53. package/lib/objects-in-viewport.d.ts +24 -0
  54. package/lib/output.capture.d.ts +24 -0
  55. package/lib/parse.keypress-kitty.d.ts +2 -0
  56. package/lib/parse.keypress.d.ts +26 -0
  57. package/lib/parse.mouse.d.ts +30 -0
  58. package/lib/paste.d.ts +7 -0
  59. package/lib/queue.d.ts +15 -0
  60. package/lib/renderable.validations.d.ts +12 -0
  61. package/lib/scroll-acceleration.d.ts +43 -0
  62. package/lib/selection.d.ts +63 -0
  63. package/lib/singleton.d.ts +7 -0
  64. package/lib/stdin-parser.d.ts +76 -0
  65. package/lib/styled-text.d.ts +63 -0
  66. package/lib/terminal-capability-detection.d.ts +30 -0
  67. package/lib/terminal-palette.d.ts +50 -0
  68. package/lib/tree-sitter/assets/update.d.ts +11 -0
  69. package/lib/tree-sitter/client.d.ts +47 -0
  70. package/lib/tree-sitter/default-parsers.d.ts +2 -0
  71. package/lib/tree-sitter/download-utils.d.ts +21 -0
  72. package/lib/tree-sitter/index.d.ts +8 -0
  73. package/lib/tree-sitter/parser.worker.d.ts +1 -0
  74. package/lib/tree-sitter/parsers-config.d.ts +38 -0
  75. package/lib/tree-sitter/resolve-ft.d.ts +2 -0
  76. package/lib/tree-sitter/types.d.ts +81 -0
  77. package/lib/tree-sitter-styled-text.d.ts +14 -0
  78. package/lib/validate-dir-name.d.ts +1 -0
  79. package/lib/yoga.options.d.ts +32 -0
  80. package/package.json +51 -63
  81. package/parser.worker.js +869 -0
  82. package/parser.worker.js.map +12 -0
  83. package/plugins/core-slot.d.ts +72 -0
  84. package/plugins/registry.d.ts +38 -0
  85. package/plugins/types.d.ts +34 -0
  86. package/post/filters.d.ts +105 -0
  87. package/renderables/ASCIIFont.d.ts +52 -0
  88. package/renderables/Box.d.ts +72 -0
  89. package/renderables/Code.d.ts +78 -0
  90. package/renderables/Diff.d.ts +142 -0
  91. package/renderables/EditBufferRenderable.d.ts +162 -0
  92. package/renderables/FrameBuffer.d.ts +16 -0
  93. package/renderables/Input.d.ts +67 -0
  94. package/renderables/LineNumberRenderable.d.ts +74 -0
  95. package/renderables/Markdown.d.ts +173 -0
  96. package/renderables/ScrollBar.d.ts +77 -0
  97. package/renderables/ScrollBox.d.ts +124 -0
  98. package/renderables/Select.d.ts +115 -0
  99. package/renderables/Slider.d.ts +44 -0
  100. package/renderables/TabSelect.d.ts +96 -0
  101. package/renderables/Text.d.ts +36 -0
  102. package/renderables/TextBufferRenderable.d.ts +105 -0
  103. package/renderables/TextNode.d.ts +91 -0
  104. package/renderables/TextTable.d.ts +140 -0
  105. package/renderables/Textarea.d.ts +114 -0
  106. package/renderables/TimeToFirstDraw.d.ts +24 -0
  107. package/renderables/__tests__/renderable-test-utils.d.ts +12 -0
  108. package/renderables/composition/VRenderable.d.ts +16 -0
  109. package/renderables/composition/constructs.d.ts +35 -0
  110. package/renderables/composition/vnode.d.ts +46 -0
  111. package/renderables/index.d.ts +22 -0
  112. package/renderables/markdown-parser.d.ts +10 -0
  113. package/renderer.d.ts +388 -0
  114. package/runtime-plugin-support.d.ts +3 -0
  115. package/runtime-plugin-support.js +29 -0
  116. package/runtime-plugin-support.js.map +10 -0
  117. package/runtime-plugin.d.ts +11 -0
  118. package/runtime-plugin.js +16 -0
  119. package/runtime-plugin.js.map +9 -0
  120. package/syntax-style.d.ts +54 -0
  121. package/testing/manual-clock.d.ts +16 -0
  122. package/testing/mock-keys.d.ts +81 -0
  123. package/testing/mock-mouse.d.ts +38 -0
  124. package/testing/mock-tree-sitter-client.d.ts +23 -0
  125. package/testing/spy.d.ts +7 -0
  126. package/testing/test-recorder.d.ts +61 -0
  127. package/testing/test-renderer.d.ts +23 -0
  128. package/testing.d.ts +6 -0
  129. package/testing.js +675 -0
  130. package/testing.js.map +15 -0
  131. package/text-buffer-view.d.ts +42 -0
  132. package/text-buffer.d.ts +67 -0
  133. package/types.d.ts +131 -0
  134. package/utils.d.ts +14 -0
  135. package/zig-structs.d.ts +155 -0
  136. package/zig.d.ts +351 -0
  137. package/dev/keypress-debug-renderer.ts +0 -148
  138. package/dev/keypress-debug.ts +0 -43
  139. package/dev/print-env-vars.ts +0 -32
  140. package/dev/test-tmux-graphics-334.sh +0 -68
  141. package/dev/thai-debug-test.ts +0 -68
  142. package/docs/development.md +0 -141
  143. package/docs/env-vars.md +0 -140
  144. package/docs/getting-started.md +0 -353
  145. package/docs/renderables-vs-constructs.md +0 -159
  146. package/docs/tree-sitter.md +0 -311
  147. package/scripts/build.ts +0 -400
  148. package/scripts/publish.ts +0 -60
  149. package/src/3d/SpriteResourceManager.ts +0 -286
  150. package/src/3d/SpriteUtils.ts +0 -71
  151. package/src/3d/TextureUtils.ts +0 -196
  152. package/src/3d/ThreeRenderable.ts +0 -197
  153. package/src/3d/WGPURenderer.ts +0 -294
  154. package/src/3d/animation/ExplodingSpriteEffect.ts +0 -513
  155. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +0 -429
  156. package/src/3d/animation/SpriteAnimator.ts +0 -633
  157. package/src/3d/animation/SpriteParticleGenerator.ts +0 -435
  158. package/src/3d/canvas.ts +0 -464
  159. package/src/3d/index.ts +0 -12
  160. package/src/3d/physics/PlanckPhysicsAdapter.ts +0 -72
  161. package/src/3d/physics/RapierPhysicsAdapter.ts +0 -66
  162. package/src/3d/physics/physics-interface.ts +0 -31
  163. package/src/3d/shaders/supersampling.wgsl +0 -201
  164. package/src/3d.ts +0 -3
  165. package/src/NativeSpanFeed.ts +0 -300
  166. package/src/Renderable.ts +0 -1698
  167. package/src/__snapshots__/buffer.test.ts.snap +0 -28
  168. package/src/animation/Timeline.test.ts +0 -2709
  169. package/src/animation/Timeline.ts +0 -598
  170. package/src/ansi.ts +0 -18
  171. package/src/benchmark/latest-all-bench-run.json +0 -707
  172. package/src/benchmark/latest-async-bench-run.json +0 -336
  173. package/src/benchmark/latest-default-bench-run.json +0 -657
  174. package/src/benchmark/latest-large-bench-run.json +0 -707
  175. package/src/benchmark/latest-quick-bench-run.json +0 -207
  176. package/src/benchmark/markdown-benchmark.ts +0 -1804
  177. package/src/benchmark/native-span-feed-async-benchmark.ts +0 -355
  178. package/src/benchmark/native-span-feed-benchmark.md +0 -56
  179. package/src/benchmark/native-span-feed-benchmark.ts +0 -596
  180. package/src/benchmark/native-span-feed-compare.ts +0 -280
  181. package/src/benchmark/renderer-benchmark.ts +0 -754
  182. package/src/benchmark/text-table-benchmark.ts +0 -947
  183. package/src/buffer.test.ts +0 -291
  184. package/src/buffer.ts +0 -519
  185. package/src/console.test.ts +0 -612
  186. package/src/console.ts +0 -1255
  187. package/src/edit-buffer.test.ts +0 -1769
  188. package/src/edit-buffer.ts +0 -411
  189. package/src/editor-view.test.ts +0 -1032
  190. package/src/editor-view.ts +0 -284
  191. package/src/examples/ascii-font-selection-demo.ts +0 -245
  192. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  193. package/src/examples/assets/concrete.png +0 -0
  194. package/src/examples/assets/crate.png +0 -0
  195. package/src/examples/assets/crate_emissive.png +0 -0
  196. package/src/examples/assets/forrest_background.png +0 -0
  197. package/src/examples/assets/hast-example.json +0 -1018
  198. package/src/examples/assets/heart.png +0 -0
  199. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  200. package/src/examples/assets/main_char_idle.png +0 -0
  201. package/src/examples/assets/main_char_jump_end.png +0 -0
  202. package/src/examples/assets/main_char_jump_landing.png +0 -0
  203. package/src/examples/assets/main_char_jump_start.png +0 -0
  204. package/src/examples/assets/main_char_run_loop.png +0 -0
  205. package/src/examples/assets/roughness_map.jpg +0 -0
  206. package/src/examples/build.ts +0 -115
  207. package/src/examples/code-demo.ts +0 -584
  208. package/src/examples/console-demo.ts +0 -358
  209. package/src/examples/core-plugin-slots-demo.ts +0 -759
  210. package/src/examples/diff-demo.ts +0 -699
  211. package/src/examples/draggable-three-demo.ts +0 -259
  212. package/src/examples/editor-demo.ts +0 -322
  213. package/src/examples/extmarks-demo.ts +0 -204
  214. package/src/examples/focus-restore-demo.ts +0 -310
  215. package/src/examples/fonts.ts +0 -245
  216. package/src/examples/fractal-shader-demo.ts +0 -268
  217. package/src/examples/framebuffer-demo.ts +0 -674
  218. package/src/examples/full-unicode-demo.ts +0 -181
  219. package/src/examples/golden-star-demo.ts +0 -933
  220. package/src/examples/grayscale-buffer-demo.ts +0 -249
  221. package/src/examples/hast-syntax-highlighting-demo.ts +0 -129
  222. package/src/examples/index.ts +0 -925
  223. package/src/examples/input-demo.ts +0 -377
  224. package/src/examples/input-select-layout-demo.ts +0 -425
  225. package/src/examples/install.sh +0 -143
  226. package/src/examples/keypress-debug-demo.ts +0 -452
  227. package/src/examples/lib/HexList.ts +0 -122
  228. package/src/examples/lib/PaletteGrid.ts +0 -125
  229. package/src/examples/lib/standalone-keys.ts +0 -25
  230. package/src/examples/lib/tab-controller.ts +0 -243
  231. package/src/examples/lights-phong-demo.ts +0 -290
  232. package/src/examples/link-demo.ts +0 -220
  233. package/src/examples/live-state-demo.ts +0 -480
  234. package/src/examples/markdown-demo.ts +0 -620
  235. package/src/examples/mouse-interaction-demo.ts +0 -428
  236. package/src/examples/nested-zindex-demo.ts +0 -357
  237. package/src/examples/opacity-example.ts +0 -235
  238. package/src/examples/opentui-demo.ts +0 -1057
  239. package/src/examples/physx-planck-2d-demo.ts +0 -507
  240. package/src/examples/physx-rapier-2d-demo.ts +0 -526
  241. package/src/examples/relative-positioning-demo.ts +0 -323
  242. package/src/examples/scroll-example.ts +0 -214
  243. package/src/examples/scrollbox-mouse-test.ts +0 -112
  244. package/src/examples/scrollbox-overlay-hit-test.ts +0 -206
  245. package/src/examples/select-demo.ts +0 -237
  246. package/src/examples/shader-cube-demo.ts +0 -772
  247. package/src/examples/simple-layout-example.ts +0 -591
  248. package/src/examples/slider-demo.ts +0 -617
  249. package/src/examples/split-mode-demo.ts +0 -445
  250. package/src/examples/sprite-animation-demo.ts +0 -443
  251. package/src/examples/sprite-particle-generator-demo.ts +0 -486
  252. package/src/examples/static-sprite-demo.ts +0 -193
  253. package/src/examples/sticky-scroll-example.ts +0 -308
  254. package/src/examples/styled-text-demo.ts +0 -282
  255. package/src/examples/tab-select-demo.ts +0 -219
  256. package/src/examples/terminal-title.ts +0 -29
  257. package/src/examples/terminal.ts +0 -305
  258. package/src/examples/text-node-demo.ts +0 -416
  259. package/src/examples/text-selection-demo.ts +0 -377
  260. package/src/examples/text-table-demo.ts +0 -503
  261. package/src/examples/text-truncation-demo.ts +0 -481
  262. package/src/examples/text-wrap.ts +0 -757
  263. package/src/examples/texture-loading-demo.ts +0 -259
  264. package/src/examples/timeline-example.ts +0 -670
  265. package/src/examples/transparency-demo.ts +0 -241
  266. package/src/examples/vnode-composition-demo.ts +0 -404
  267. package/src/index.ts +0 -22
  268. package/src/lib/KeyHandler.integration.test.ts +0 -292
  269. package/src/lib/KeyHandler.stopPropagation.test.ts +0 -289
  270. package/src/lib/KeyHandler.test.ts +0 -662
  271. package/src/lib/KeyHandler.ts +0 -222
  272. package/src/lib/RGBA.test.ts +0 -984
  273. package/src/lib/RGBA.ts +0 -204
  274. package/src/lib/ascii.font.ts +0 -330
  275. package/src/lib/border.test.ts +0 -83
  276. package/src/lib/border.ts +0 -168
  277. package/src/lib/bunfs.test.ts +0 -27
  278. package/src/lib/bunfs.ts +0 -18
  279. package/src/lib/clipboard.test.ts +0 -41
  280. package/src/lib/clipboard.ts +0 -47
  281. package/src/lib/clock.ts +0 -31
  282. package/src/lib/data-paths.test.ts +0 -133
  283. package/src/lib/data-paths.ts +0 -109
  284. package/src/lib/debounce.ts +0 -106
  285. package/src/lib/detect-links.test.ts +0 -98
  286. package/src/lib/detect-links.ts +0 -56
  287. package/src/lib/env.test.ts +0 -228
  288. package/src/lib/env.ts +0 -209
  289. package/src/lib/extmarks-history.ts +0 -51
  290. package/src/lib/extmarks-multiwidth.test.ts +0 -322
  291. package/src/lib/extmarks.test.ts +0 -3457
  292. package/src/lib/extmarks.ts +0 -843
  293. package/src/lib/fonts/block.json +0 -405
  294. package/src/lib/fonts/grid.json +0 -265
  295. package/src/lib/fonts/huge.json +0 -741
  296. package/src/lib/fonts/pallet.json +0 -314
  297. package/src/lib/fonts/shade.json +0 -591
  298. package/src/lib/fonts/slick.json +0 -321
  299. package/src/lib/fonts/tiny.json +0 -69
  300. package/src/lib/hast-styled-text.ts +0 -59
  301. package/src/lib/index.ts +0 -21
  302. package/src/lib/keymapping.test.ts +0 -280
  303. package/src/lib/keymapping.ts +0 -87
  304. package/src/lib/objects-in-viewport.test.ts +0 -787
  305. package/src/lib/objects-in-viewport.ts +0 -153
  306. package/src/lib/output.capture.ts +0 -58
  307. package/src/lib/parse.keypress-kitty.protocol.test.ts +0 -340
  308. package/src/lib/parse.keypress-kitty.test.ts +0 -663
  309. package/src/lib/parse.keypress-kitty.ts +0 -439
  310. package/src/lib/parse.keypress.test.ts +0 -1849
  311. package/src/lib/parse.keypress.ts +0 -397
  312. package/src/lib/parse.mouse.test.ts +0 -552
  313. package/src/lib/parse.mouse.ts +0 -232
  314. package/src/lib/paste.ts +0 -16
  315. package/src/lib/queue.ts +0 -65
  316. package/src/lib/renderable.validations.test.ts +0 -87
  317. package/src/lib/renderable.validations.ts +0 -83
  318. package/src/lib/scroll-acceleration.ts +0 -98
  319. package/src/lib/selection.ts +0 -240
  320. package/src/lib/singleton.ts +0 -28
  321. package/src/lib/stdin-parser.test.ts +0 -1676
  322. package/src/lib/stdin-parser.ts +0 -1248
  323. package/src/lib/styled-text.ts +0 -178
  324. package/src/lib/terminal-capability-detection.test.ts +0 -202
  325. package/src/lib/terminal-capability-detection.ts +0 -79
  326. package/src/lib/terminal-palette.test.ts +0 -878
  327. package/src/lib/terminal-palette.ts +0 -383
  328. package/src/lib/tree-sitter/assets/README.md +0 -118
  329. package/src/lib/tree-sitter/assets/update.ts +0 -331
  330. package/src/lib/tree-sitter/assets.d.ts +0 -9
  331. package/src/lib/tree-sitter/cache.test.ts +0 -270
  332. package/src/lib/tree-sitter/client.test.ts +0 -1061
  333. package/src/lib/tree-sitter/client.ts +0 -615
  334. package/src/lib/tree-sitter/default-parsers.ts +0 -80
  335. package/src/lib/tree-sitter/download-utils.ts +0 -148
  336. package/src/lib/tree-sitter/index.ts +0 -28
  337. package/src/lib/tree-sitter/parser.worker.ts +0 -1001
  338. package/src/lib/tree-sitter/parsers-config.ts +0 -75
  339. package/src/lib/tree-sitter/resolve-ft.ts +0 -62
  340. package/src/lib/tree-sitter/types.ts +0 -81
  341. package/src/lib/tree-sitter-styled-text.test.ts +0 -1253
  342. package/src/lib/tree-sitter-styled-text.ts +0 -306
  343. package/src/lib/validate-dir-name.ts +0 -55
  344. package/src/lib/yoga.options.test.ts +0 -628
  345. package/src/lib/yoga.options.ts +0 -346
  346. package/src/plugins/core-slot.ts +0 -579
  347. package/src/plugins/registry.ts +0 -377
  348. package/src/plugins/types.ts +0 -46
  349. package/src/post/filters.ts +0 -888
  350. package/src/renderables/ASCIIFont.ts +0 -219
  351. package/src/renderables/Box.test.ts +0 -160
  352. package/src/renderables/Box.ts +0 -295
  353. package/src/renderables/Code.test.ts +0 -2062
  354. package/src/renderables/Code.ts +0 -357
  355. package/src/renderables/Diff.regression.test.ts +0 -226
  356. package/src/renderables/Diff.test.ts +0 -3027
  357. package/src/renderables/Diff.ts +0 -1209
  358. package/src/renderables/EditBufferRenderable.ts +0 -764
  359. package/src/renderables/FrameBuffer.ts +0 -47
  360. package/src/renderables/Input.test.ts +0 -1228
  361. package/src/renderables/Input.ts +0 -245
  362. package/src/renderables/LineNumberRenderable.ts +0 -675
  363. package/src/renderables/Markdown.ts +0 -1106
  364. package/src/renderables/ScrollBar.ts +0 -422
  365. package/src/renderables/ScrollBox.ts +0 -883
  366. package/src/renderables/Select.test.ts +0 -1010
  367. package/src/renderables/Select.ts +0 -523
  368. package/src/renderables/Slider.test.ts +0 -456
  369. package/src/renderables/Slider.ts +0 -347
  370. package/src/renderables/TabSelect.test.ts +0 -197
  371. package/src/renderables/TabSelect.ts +0 -455
  372. package/src/renderables/Text.selection-buffer.test.ts +0 -123
  373. package/src/renderables/Text.test.ts +0 -2660
  374. package/src/renderables/Text.ts +0 -147
  375. package/src/renderables/TextBufferRenderable.ts +0 -518
  376. package/src/renderables/TextNode.test.ts +0 -1058
  377. package/src/renderables/TextNode.ts +0 -325
  378. package/src/renderables/TextTable.test.ts +0 -1421
  379. package/src/renderables/TextTable.ts +0 -1344
  380. package/src/renderables/Textarea.ts +0 -732
  381. package/src/renderables/TimeToFirstDraw.ts +0 -89
  382. package/src/renderables/__snapshots__/Code.test.ts.snap +0 -13
  383. package/src/renderables/__snapshots__/Diff.test.ts.snap +0 -785
  384. package/src/renderables/__snapshots__/Text.test.ts.snap +0 -421
  385. package/src/renderables/__snapshots__/TextTable.test.ts.snap +0 -215
  386. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +0 -144
  387. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +0 -816
  388. package/src/renderables/__tests__/LineNumberRenderable.test.ts +0 -1787
  389. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +0 -85
  390. package/src/renderables/__tests__/Markdown.test.ts +0 -2287
  391. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +0 -87
  392. package/src/renderables/__tests__/Textarea.buffer.test.ts +0 -682
  393. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +0 -675
  394. package/src/renderables/__tests__/Textarea.editing.test.ts +0 -2041
  395. package/src/renderables/__tests__/Textarea.error-handling.test.ts +0 -35
  396. package/src/renderables/__tests__/Textarea.events.test.ts +0 -738
  397. package/src/renderables/__tests__/Textarea.highlights.test.ts +0 -590
  398. package/src/renderables/__tests__/Textarea.keybinding.test.ts +0 -3149
  399. package/src/renderables/__tests__/Textarea.paste.test.ts +0 -357
  400. package/src/renderables/__tests__/Textarea.rendering.test.ts +0 -1864
  401. package/src/renderables/__tests__/Textarea.scroll.test.ts +0 -733
  402. package/src/renderables/__tests__/Textarea.selection.test.ts +0 -1590
  403. package/src/renderables/__tests__/Textarea.stress.test.ts +0 -670
  404. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +0 -383
  405. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +0 -310
  406. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +0 -221
  407. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +0 -89
  408. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +0 -457
  409. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +0 -158
  410. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +0 -387
  411. package/src/renderables/__tests__/markdown-parser.test.ts +0 -217
  412. package/src/renderables/__tests__/renderable-test-utils.ts +0 -60
  413. package/src/renderables/composition/README.md +0 -8
  414. package/src/renderables/composition/VRenderable.ts +0 -32
  415. package/src/renderables/composition/constructs.ts +0 -127
  416. package/src/renderables/composition/vnode.ts +0 -289
  417. package/src/renderables/index.ts +0 -22
  418. package/src/renderables/markdown-parser.ts +0 -66
  419. package/src/renderer.ts +0 -2363
  420. package/src/runtime-plugin-support.ts +0 -39
  421. package/src/runtime-plugin.ts +0 -144
  422. package/src/syntax-style.test.ts +0 -841
  423. package/src/syntax-style.ts +0 -264
  424. package/src/testing/README.md +0 -210
  425. package/src/testing/capture-spans.test.ts +0 -194
  426. package/src/testing/integration.test.ts +0 -276
  427. package/src/testing/manual-clock.ts +0 -106
  428. package/src/testing/mock-keys.test.ts +0 -1356
  429. package/src/testing/mock-keys.ts +0 -449
  430. package/src/testing/mock-mouse.test.ts +0 -218
  431. package/src/testing/mock-mouse.ts +0 -247
  432. package/src/testing/mock-tree-sitter-client.ts +0 -73
  433. package/src/testing/spy.ts +0 -13
  434. package/src/testing/test-recorder.test.ts +0 -415
  435. package/src/testing/test-recorder.ts +0 -145
  436. package/src/testing/test-renderer.ts +0 -116
  437. package/src/testing.ts +0 -7
  438. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +0 -481
  439. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +0 -19
  440. package/src/tests/__snapshots__/scrollbox.test.ts.snap +0 -29
  441. package/src/tests/absolute-positioning.snapshot.test.ts +0 -638
  442. package/src/tests/allocator-stats.test.ts +0 -38
  443. package/src/tests/destroy-during-render.test.ts +0 -200
  444. package/src/tests/hover-cursor.test.ts +0 -98
  445. package/src/tests/native-span-feed-async.test.ts +0 -173
  446. package/src/tests/native-span-feed-close.test.ts +0 -120
  447. package/src/tests/native-span-feed-coverage.test.ts +0 -227
  448. package/src/tests/native-span-feed-edge-cases.test.ts +0 -352
  449. package/src/tests/native-span-feed-use-after-free.test.ts +0 -45
  450. package/src/tests/opacity.test.ts +0 -123
  451. package/src/tests/renderable.snapshot.test.ts +0 -524
  452. package/src/tests/renderable.test.ts +0 -1281
  453. package/src/tests/renderer.console-startup.test.ts +0 -65
  454. package/src/tests/renderer.control.test.ts +0 -364
  455. package/src/tests/renderer.core-slot-binding.test.ts +0 -952
  456. package/src/tests/renderer.cursor.test.ts +0 -26
  457. package/src/tests/renderer.destroy-during-render.test.ts +0 -110
  458. package/src/tests/renderer.focus-restore.test.ts +0 -228
  459. package/src/tests/renderer.focus.test.ts +0 -251
  460. package/src/tests/renderer.idle.test.ts +0 -219
  461. package/src/tests/renderer.input.test.ts +0 -2145
  462. package/src/tests/renderer.kitty-flags.test.ts +0 -195
  463. package/src/tests/renderer.mouse.test.ts +0 -1269
  464. package/src/tests/renderer.palette.test.ts +0 -629
  465. package/src/tests/renderer.selection.test.ts +0 -49
  466. package/src/tests/renderer.slot-registry.test.ts +0 -649
  467. package/src/tests/renderer.useMouse.test.ts +0 -50
  468. package/src/tests/runtime-plugin-support.fixture.ts +0 -11
  469. package/src/tests/runtime-plugin-support.test.ts +0 -28
  470. package/src/tests/runtime-plugin.fixture.ts +0 -40
  471. package/src/tests/runtime-plugin.test.ts +0 -190
  472. package/src/tests/scrollbox-culling-bug.test.ts +0 -114
  473. package/src/tests/scrollbox-hitgrid-resize.test.ts +0 -136
  474. package/src/tests/scrollbox-hitgrid.test.ts +0 -909
  475. package/src/tests/scrollbox.test.ts +0 -1530
  476. package/src/tests/wrap-resize-perf.test.ts +0 -229
  477. package/src/tests/yoga-setters.test.ts +0 -921
  478. package/src/text-buffer-view.test.ts +0 -705
  479. package/src/text-buffer-view.ts +0 -189
  480. package/src/text-buffer.test.ts +0 -347
  481. package/src/text-buffer.ts +0 -250
  482. package/src/types.ts +0 -152
  483. package/src/utils.ts +0 -88
  484. package/src/zig/ansi.zig +0 -268
  485. package/src/zig/bench/README.md +0 -50
  486. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +0 -887
  487. package/src/zig/bench/edit-buffer_bench.zig +0 -476
  488. package/src/zig/bench/native-span-feed_bench.zig +0 -100
  489. package/src/zig/bench/rope-markers_bench.zig +0 -713
  490. package/src/zig/bench/rope_bench.zig +0 -514
  491. package/src/zig/bench/styled-text_bench.zig +0 -470
  492. package/src/zig/bench/text-buffer-coords_bench.zig +0 -362
  493. package/src/zig/bench/text-buffer-view_bench.zig +0 -459
  494. package/src/zig/bench/text-chunk-graphemes_bench.zig +0 -273
  495. package/src/zig/bench/utf8_bench.zig +0 -799
  496. package/src/zig/bench-utils.zig +0 -431
  497. package/src/zig/bench.zig +0 -217
  498. package/src/zig/buffer.zig +0 -2223
  499. package/src/zig/build.zig +0 -289
  500. package/src/zig/build.zig.zon +0 -16
  501. package/src/zig/edit-buffer.zig +0 -825
  502. package/src/zig/editor-view.zig +0 -802
  503. package/src/zig/event-bus.zig +0 -13
  504. package/src/zig/event-emitter.zig +0 -65
  505. package/src/zig/file-logger.zig +0 -92
  506. package/src/zig/grapheme.zig +0 -599
  507. package/src/zig/lib.zig +0 -1834
  508. package/src/zig/link.zig +0 -333
  509. package/src/zig/logger.zig +0 -43
  510. package/src/zig/mem-registry.zig +0 -125
  511. package/src/zig/native-span-feed-bench-lib.zig +0 -7
  512. package/src/zig/native-span-feed.zig +0 -708
  513. package/src/zig/renderer.zig +0 -1386
  514. package/src/zig/rope.zig +0 -1220
  515. package/src/zig/syntax-style.zig +0 -161
  516. package/src/zig/terminal.zig +0 -975
  517. package/src/zig/test.zig +0 -70
  518. package/src/zig/tests/README.md +0 -18
  519. package/src/zig/tests/buffer_test.zig +0 -2526
  520. package/src/zig/tests/edit-buffer-history_test.zig +0 -271
  521. package/src/zig/tests/edit-buffer_test.zig +0 -1689
  522. package/src/zig/tests/editor-view_test.zig +0 -3299
  523. package/src/zig/tests/event-emitter_test.zig +0 -249
  524. package/src/zig/tests/grapheme_test.zig +0 -1304
  525. package/src/zig/tests/link_test.zig +0 -190
  526. package/src/zig/tests/mem-registry_test.zig +0 -473
  527. package/src/zig/tests/memory_leak_regression_test.zig +0 -159
  528. package/src/zig/tests/native-span-feed_test.zig +0 -1264
  529. package/src/zig/tests/renderer_test.zig +0 -1010
  530. package/src/zig/tests/rope-nested_test.zig +0 -712
  531. package/src/zig/tests/rope_fuzz_test.zig +0 -238
  532. package/src/zig/tests/rope_test.zig +0 -2362
  533. package/src/zig/tests/segment-merge.test.zig +0 -148
  534. package/src/zig/tests/syntax-style_test.zig +0 -557
  535. package/src/zig/tests/terminal_test.zig +0 -719
  536. package/src/zig/tests/text-buffer-drawing_test.zig +0 -3237
  537. package/src/zig/tests/text-buffer-highlights_test.zig +0 -666
  538. package/src/zig/tests/text-buffer-iterators_test.zig +0 -776
  539. package/src/zig/tests/text-buffer-segment_test.zig +0 -320
  540. package/src/zig/tests/text-buffer-selection_test.zig +0 -1035
  541. package/src/zig/tests/text-buffer-selection_viewport_test.zig +0 -358
  542. package/src/zig/tests/text-buffer-view_test.zig +0 -3649
  543. package/src/zig/tests/text-buffer_test.zig +0 -2191
  544. package/src/zig/tests/unicode-width-map.zon +0 -3909
  545. package/src/zig/tests/utf8_no_zwj_test.zig +0 -260
  546. package/src/zig/tests/utf8_test.zig +0 -4057
  547. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +0 -267
  548. package/src/zig/tests/utf8_wcwidth_test.zig +0 -357
  549. package/src/zig/tests/word-wrap-editing_test.zig +0 -498
  550. package/src/zig/tests/wrap-cache-perf_test.zig +0 -113
  551. package/src/zig/text-buffer-iterators.zig +0 -499
  552. package/src/zig/text-buffer-segment.zig +0 -404
  553. package/src/zig/text-buffer-view.zig +0 -1371
  554. package/src/zig/text-buffer.zig +0 -1180
  555. package/src/zig/utf8.zig +0 -1948
  556. package/src/zig/utils.zig +0 -9
  557. package/src/zig-structs.ts +0 -261
  558. package/src/zig.ts +0 -3843
  559. package/tsconfig.build.json +0 -22
  560. package/tsconfig.json +0 -28
  561. /package/{src/lib/tree-sitter/assets → assets}/javascript/highlights.scm +0 -0
  562. /package/{src/lib/tree-sitter/assets → assets}/javascript/tree-sitter-javascript.wasm +0 -0
  563. /package/{src/lib/tree-sitter/assets → assets}/markdown/highlights.scm +0 -0
  564. /package/{src/lib/tree-sitter/assets → assets}/markdown/injections.scm +0 -0
  565. /package/{src/lib/tree-sitter/assets → assets}/markdown/tree-sitter-markdown.wasm +0 -0
  566. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/highlights.scm +0 -0
  567. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  568. /package/{src/lib/tree-sitter/assets → assets}/typescript/highlights.scm +0 -0
  569. /package/{src/lib/tree-sitter/assets → assets}/typescript/tree-sitter-typescript.wasm +0 -0
  570. /package/{src/lib/tree-sitter/assets → assets}/zig/highlights.scm +0 -0
  571. /package/{src/lib/tree-sitter/assets → assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -1,2287 +0,0 @@
1
- import { test, expect, beforeAll, beforeEach, afterEach, afterAll } from "bun:test"
2
- import { MarkdownRenderable, type MarkdownOptions } from "../Markdown.js"
3
- import { CodeRenderable } from "../Code.js"
4
- import { TextRenderable } from "../Text.js"
5
- import { TextTableRenderable } from "../TextTable.js"
6
- import { SyntaxStyle } from "../../syntax-style.js"
7
- import { RGBA } from "../../lib/RGBA.js"
8
- import { TreeSitterClient } from "../../lib/tree-sitter/index.js"
9
- import { tmpdir } from "node:os"
10
- import { join } from "node:path"
11
- import { mkdir } from "node:fs/promises"
12
- import {
13
- createTestRenderer,
14
- type MockMouse,
15
- type TestRenderer,
16
- MockTreeSitterClient,
17
- TestRecorder,
18
- } from "../../testing.js"
19
- import { TextAttributes, type CapturedFrame } from "../../types.js"
20
-
21
- let renderer: TestRenderer
22
- let mockMouse: MockMouse
23
- let renderOnce: () => Promise<void>
24
- let captureFrame: () => string
25
- let captureSpans: () => CapturedFrame
26
- let markdownTreeSitterClient: TreeSitterClient
27
-
28
- const syntaxStyle = SyntaxStyle.fromStyles({
29
- default: { fg: RGBA.fromValues(1, 1, 1, 1) },
30
- })
31
-
32
- beforeAll(async () => {
33
- const dataPath = join(tmpdir(), "tree-sitter-markdown-renderable-test-data")
34
- await mkdir(dataPath, { recursive: true })
35
-
36
- markdownTreeSitterClient = new TreeSitterClient({ dataPath })
37
- await markdownTreeSitterClient.initialize()
38
- })
39
-
40
- beforeEach(async () => {
41
- const testRenderer = await createTestRenderer({ width: 60, height: 40 })
42
- renderer = testRenderer.renderer
43
- mockMouse = testRenderer.mockMouse
44
- renderOnce = testRenderer.renderOnce
45
- captureFrame = testRenderer.captureCharFrame
46
- captureSpans = testRenderer.captureSpans
47
- })
48
-
49
- afterEach(async () => {
50
- if (renderer) {
51
- renderer.destroy()
52
- }
53
- })
54
-
55
- afterAll(async () => {
56
- await markdownTreeSitterClient.destroy()
57
- })
58
-
59
- function createMarkdownRenderable(options: MarkdownOptions): MarkdownRenderable {
60
- return new MarkdownRenderable(renderer, {
61
- treeSitterClient: markdownTreeSitterClient,
62
- ...options,
63
- })
64
- }
65
-
66
- async function renderMarkdownRenderable(md: MarkdownRenderable, timeoutMs: number = 2000): Promise<void> {
67
- const hasPendingMarkdownParagraphHighlights = (): boolean =>
68
- md
69
- .getChildren()
70
- .some((child) => child instanceof CodeRenderable && child.filetype === "markdown" && child.isHighlighting)
71
-
72
- const startedAt = Date.now()
73
-
74
- await renderOnce()
75
-
76
- while (hasPendingMarkdownParagraphHighlights() && Date.now() - startedAt < timeoutMs) {
77
- await Bun.sleep(10)
78
- await renderOnce()
79
- }
80
-
81
- if (hasPendingMarkdownParagraphHighlights()) {
82
- throw new Error("Timed out waiting for markdown paragraph highlights")
83
- }
84
-
85
- await renderOnce()
86
- }
87
-
88
- async function renderMarkdown(markdown: string, conceal: boolean = true): Promise<string> {
89
- const md = createMarkdownRenderable({
90
- id: "markdown",
91
- content: markdown,
92
- syntaxStyle,
93
- conceal,
94
- tableOptions: { widthMode: "content" },
95
- })
96
-
97
- renderer.root.add(md)
98
- await renderMarkdownRenderable(md)
99
-
100
- const lines = captureFrame()
101
- .split("\n")
102
- .map((line) => line.trimEnd())
103
- return "\n" + lines.join("\n").trimEnd()
104
- }
105
-
106
- test("basic table alignment", async () => {
107
- const markdown = `| Name | Age |
108
- |---|---|
109
- | Alice | 30 |
110
- | Bob | 5 |`
111
-
112
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
113
- "
114
- ┌─────┬───┐
115
- │Name │Age│
116
- ├─────┼───┤
117
- │Alice│30 │
118
- ├─────┼───┤
119
- │Bob │5 │
120
- └─────┴───┘"
121
- `)
122
- })
123
-
124
- test("tableOptions.widthMode configures markdown table layout", async () => {
125
- const md = createMarkdownRenderable({
126
- id: "markdown-table-width-mode",
127
- content: "| Name | Age |\n|---|---|\n| Alice | 30 |",
128
- syntaxStyle,
129
- tableOptions: {
130
- widthMode: "full",
131
- columnFitter: "balanced",
132
- },
133
- })
134
-
135
- renderer.root.add(md)
136
- await renderer.idle()
137
-
138
- const table = md._blockStates[0]?.renderable as TextTableRenderable
139
- expect(table).toBeInstanceOf(TextTableRenderable)
140
- expect(table.columnWidthMode).toBe("full")
141
- expect(table.columnFitter).toBe("balanced")
142
- })
143
-
144
- test("tableOptions updates existing markdown table renderable", async () => {
145
- const md = createMarkdownRenderable({
146
- id: "markdown-table-updates",
147
- content: "| Name | Age |\n|---|---|\n| Alice | 30 |",
148
- syntaxStyle,
149
- })
150
-
151
- renderer.root.add(md)
152
- await renderer.idle()
153
-
154
- const table = md._blockStates[0]?.renderable as TextTableRenderable
155
- expect(table).toBeInstanceOf(TextTableRenderable)
156
- expect(table.columnWidthMode).toBe("full")
157
-
158
- md.tableOptions = {
159
- widthMode: "full",
160
- columnFitter: "balanced",
161
- wrapMode: "word",
162
- cellPadding: 1,
163
- borders: false,
164
- selectable: false,
165
- }
166
-
167
- await renderer.idle()
168
-
169
- const updatedTable = md._blockStates[0]?.renderable as TextTableRenderable
170
- expect(updatedTable).toBe(table)
171
- expect(updatedTable.columnWidthMode).toBe("full")
172
- expect(updatedTable.columnFitter).toBe("balanced")
173
- expect(updatedTable.wrapMode).toBe("word")
174
- expect(updatedTable.cellPadding).toBe(1)
175
- expect(updatedTable.border).toBe(false)
176
- expect(updatedTable.outerBorder).toBe(false)
177
- expect(updatedTable.showBorders).toBe(false)
178
- expect(updatedTable.selectable).toBe(false)
179
- })
180
-
181
- test("table with inline code (backticks)", async () => {
182
- const markdown = `| Command | Description |
183
- |---|---|
184
- | \`npm install\` | Install deps |
185
- | \`npm run build\` | Build project |
186
- | \`npm test\` | Run tests |`
187
-
188
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
189
- "
190
- ┌─────────────┬─────────────┐
191
- │Command │Description │
192
- ├─────────────┼─────────────┤
193
- │npm install │Install deps │
194
- ├─────────────┼─────────────┤
195
- │npm run build│Build project│
196
- ├─────────────┼─────────────┤
197
- │npm test │Run tests │
198
- └─────────────┴─────────────┘"
199
- `)
200
- })
201
-
202
- test("table with bold text", async () => {
203
- const markdown = `| Feature | Status |
204
- |---|---|
205
- | **Authentication** | Done |
206
- | **API** | WIP |`
207
-
208
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
209
- "
210
- ┌──────────────┬──────┐
211
- │Feature │Status│
212
- ├──────────────┼──────┤
213
- │Authentication│Done │
214
- ├──────────────┼──────┤
215
- │API │WIP │
216
- └──────────────┴──────┘"
217
- `)
218
- })
219
-
220
- test("table with italic text", async () => {
221
- const markdown = `| Item | Note |
222
- |---|---|
223
- | One | *important* |
224
- | Two | *ok* |`
225
-
226
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
227
- "
228
- ┌────┬─────────┐
229
- │Item│Note │
230
- ├────┼─────────┤
231
- │One │important│
232
- ├────┼─────────┤
233
- │Two │ok │
234
- └────┴─────────┘"
235
- `)
236
- })
237
-
238
- test("table with mixed formatting", async () => {
239
- const markdown = `| Type | Value | Notes |
240
- |---|---|---|
241
- | **Bold** | \`code\` | *italic* |
242
- | Plain | **strong** | \`cmd\` |`
243
-
244
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
245
- "
246
- ┌─────┬──────┬──────┐
247
- │Type │Value │Notes │
248
- ├─────┼──────┼──────┤
249
- │Bold │code │italic│
250
- ├─────┼──────┼──────┤
251
- │Plain│strong│cmd │
252
- └─────┴──────┴──────┘"
253
- `)
254
- })
255
-
256
- test("table with alignment markers (left, center, right)", async () => {
257
- const markdown = `| Left | Center | Right |
258
- |:---|:---:|---:|
259
- | A | B | C |
260
- | Long text | X | Y |`
261
-
262
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
263
- "
264
- ┌─────────┬──────┬─────┐
265
- │Left │Center│Right│
266
- ├─────────┼──────┼─────┤
267
- │A │B │C │
268
- ├─────────┼──────┼─────┤
269
- │Long text│X │Y │
270
- └─────────┴──────┴─────┘"
271
- `)
272
- })
273
-
274
- test("table with empty cells", async () => {
275
- const markdown = `| A | B |
276
- |---|---|
277
- | X | |
278
- | | Y |`
279
-
280
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
281
- "
282
- ┌─┬─┐
283
- │A│B│
284
- ├─┼─┤
285
- │X│ │
286
- ├─┼─┤
287
- │ │Y│
288
- └─┴─┘"
289
- `)
290
- })
291
-
292
- test("table with long header and short content", async () => {
293
- const markdown = `| Very Long Column Header | Short |
294
- |---|---|
295
- | A | B |`
296
-
297
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
298
- "
299
- ┌───────────────────────┬─────┐
300
- │Very Long Column Header│Short│
301
- ├───────────────────────┼─────┤
302
- │A │B │
303
- └───────────────────────┴─────┘"
304
- `)
305
- })
306
-
307
- test("table with short header and long content", async () => {
308
- const markdown = `| X | Y |
309
- |---|---|
310
- | This is very long content | Short |`
311
-
312
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
313
- "
314
- ┌─────────────────────────┬─────┐
315
- │X │Y │
316
- ├─────────────────────────┼─────┤
317
- │This is very long content│Short│
318
- └─────────────────────────┴─────┘"
319
- `)
320
- })
321
-
322
- test("table inside code block should NOT be formatted", async () => {
323
- const markdown = `\`\`\`
324
- | Not | A | Table |
325
- |---|---|---|
326
- | Should | Stay | Raw |
327
- \`\`\`
328
-
329
- | Real | Table |
330
- |---|---|
331
- | Is | Formatted |`
332
-
333
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
334
- "
335
- | Not | A | Table |
336
- |---|---|---|
337
- | Should | Stay | Raw |
338
-
339
- ┌────┬─────────┐
340
- │Real│Table │
341
- ├────┼─────────┤
342
- │Is │Formatted│
343
- └────┴─────────┘"
344
- `)
345
- })
346
-
347
- test("multiple tables in same document", async () => {
348
- const markdown = `| Table1 | A |
349
- |---|---|
350
- | X | Y |
351
-
352
- Some text between.
353
-
354
- | Table2 | BB |
355
- |---|---|
356
- | Long content | Z |`
357
-
358
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
359
- "
360
- ┌──────┬─┐
361
- │Table1│A│
362
- ├──────┼─┤
363
- │X │Y│
364
- └──────┴─┘
365
-
366
- Some text between.
367
- ┌────────────┬──┐
368
- │Table2 │BB│
369
- ├────────────┼──┤
370
- │Long content│Z │
371
- └────────────┴──┘"
372
- `)
373
- })
374
-
375
- test("table with escaped pipe character", async () => {
376
- const markdown = `| Command | Output |
377
- |---|---|
378
- | echo | Hello |
379
- | ls \\| grep | Filtered |`
380
-
381
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
382
- "
383
- ┌─────────┬────────┐
384
- │Command │Output │
385
- ├─────────┼────────┤
386
- │echo │Hello │
387
- ├─────────┼────────┤
388
- │ls | grep│Filtered│
389
- └─────────┴────────┘"
390
- `)
391
- })
392
-
393
- test("table with unicode characters", async () => {
394
- const markdown = `| Emoji | Name |
395
- |---|---|
396
- | 🎉 | Party |
397
- | 🚀 | Rocket |
398
- | 日本語 | Japanese |`
399
-
400
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
401
- "
402
- ┌──────┬────────┐
403
- │Emoji │Name │
404
- ├──────┼────────┤
405
- │🎉 │Party │
406
- ├──────┼────────┤
407
- │🚀 │Rocket │
408
- ├──────┼────────┤
409
- │日本語│Japanese│
410
- └──────┴────────┘"
411
- `)
412
- })
413
-
414
- test("table with links", async () => {
415
- const markdown = `| Name | Link |
416
- |---|---|
417
- | Google | [link](https://google.com) |
418
- | GitHub | [gh](https://github.com) |`
419
-
420
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
421
- "
422
- ┌──────┬─────────────────────────┐
423
- │Name │Link │
424
- ├──────┼─────────────────────────┤
425
- │Google│link (https://google.com)│
426
- ├──────┼─────────────────────────┤
427
- │GitHub│gh (https://github.com) │
428
- └──────┴─────────────────────────┘"
429
- `)
430
- })
431
-
432
- test("single row table (header + delimiter only)", async () => {
433
- const markdown = `| Only | Header |
434
- |---|---|`
435
-
436
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
437
- "
438
- | Only | Header |
439
- |---|---|"
440
- `)
441
- })
442
-
443
- test("table with many columns", async () => {
444
- const markdown = `| A | B | C | D | E |
445
- |---|---|---|---|---|
446
- | 1 | 2 | 3 | 4 | 5 |`
447
-
448
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
449
- "
450
- ┌─┬─┬─┬─┬─┐
451
- │A│B│C│D│E│
452
- ├─┼─┼─┼─┼─┤
453
- │1│2│3│4│5│
454
- └─┴─┴─┴─┴─┘"
455
- `)
456
- })
457
-
458
- test("no tables returns original content", async () => {
459
- const markdown = `# Just a heading
460
-
461
- Some paragraph text.
462
-
463
- - List item`
464
-
465
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
466
- "
467
- Just a heading
468
-
469
- Some paragraph text.
470
-
471
- - List item"
472
- `)
473
- })
474
-
475
- test("table with nested inline formatting", async () => {
476
- const markdown = `| Description |
477
- |---|
478
- | This has **bold and \`code\`** together |
479
- | And *italic with **nested bold*** |`
480
-
481
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
482
- "
483
- ┌───────────────────────────────┐
484
- │Description │
485
- ├───────────────────────────────┤
486
- │This has bold and code together│
487
- ├───────────────────────────────┤
488
- │And italic with nested bold │
489
- └───────────────────────────────┘"
490
- `)
491
- })
492
-
493
- // Tests with conceal=false - formatting markers should be visible and columns sized accordingly
494
-
495
- test("conceal=false: table with bold text", async () => {
496
- const markdown = `| Feature | Status |
497
- |---|---|
498
- | **Authentication** | Done |
499
- | **API** | WIP |`
500
-
501
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
502
- "
503
- ┌──────────────────┬──────┐
504
- │Feature │Status│
505
- ├──────────────────┼──────┤
506
- │**Authentication**│Done │
507
- ├──────────────────┼──────┤
508
- │**API** │WIP │
509
- └──────────────────┴──────┘"
510
- `)
511
- })
512
-
513
- test("conceal=false: table with inline code", async () => {
514
- const markdown = `| Command | Description |
515
- |---|---|
516
- | \`npm install\` | Install deps |
517
- | \`npm run build\` | Build project |`
518
-
519
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
520
- "
521
- ┌───────────────┬─────────────┐
522
- │Command │Description │
523
- ├───────────────┼─────────────┤
524
- │\`npm install\` │Install deps │
525
- ├───────────────┼─────────────┤
526
- │\`npm run build\`│Build project│
527
- └───────────────┴─────────────┘"
528
- `)
529
- })
530
-
531
- test("conceal=false: table with italic text", async () => {
532
- const markdown = `| Item | Note |
533
- |---|---|
534
- | One | *important* |
535
- | Two | *ok* |`
536
-
537
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
538
- "
539
- ┌────┬───────────┐
540
- │Item│Note │
541
- ├────┼───────────┤
542
- │One │*important*│
543
- ├────┼───────────┤
544
- │Two │*ok* │
545
- └────┴───────────┘"
546
- `)
547
- })
548
-
549
- test("conceal=false: table with mixed formatting", async () => {
550
- const markdown = `| Type | Value | Notes |
551
- |---|---|---|
552
- | **Bold** | \`code\` | *italic* |
553
- | Plain | **strong** | \`cmd\` |`
554
-
555
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
556
- "
557
- ┌────────┬──────────┬────────┐
558
- │Type │Value │Notes │
559
- ├────────┼──────────┼────────┤
560
- │**Bold**│\`code\` │*italic*│
561
- ├────────┼──────────┼────────┤
562
- │Plain │**strong**│\`cmd\` │
563
- └────────┴──────────┴────────┘"
564
- `)
565
- })
566
-
567
- test("conceal=false: table with unicode characters", async () => {
568
- const markdown = `| Emoji | Name |
569
- |---|---|
570
- | 🎉 | Party |
571
- | 🚀 | Rocket |
572
- | 日本語 | Japanese |`
573
-
574
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
575
- "
576
- ┌──────┬────────┐
577
- │Emoji │Name │
578
- ├──────┼────────┤
579
- │🎉 │Party │
580
- ├──────┼────────┤
581
- │🚀 │Rocket │
582
- ├──────┼────────┤
583
- │日本語│Japanese│
584
- └──────┴────────┘"
585
- `)
586
- })
587
-
588
- test("conceal=false: basic table alignment", async () => {
589
- const markdown = `| Name | Age |
590
- |---|---|
591
- | Alice | 30 |
592
- | Bob | 5 |`
593
-
594
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
595
- "
596
- ┌─────┬───┐
597
- │Name │Age│
598
- ├─────┼───┤
599
- │Alice│30 │
600
- ├─────┼───┤
601
- │Bob │5 │
602
- └─────┴───┘"
603
- `)
604
- })
605
-
606
- test("table with paragraphs before and after", async () => {
607
- const markdown = `This is a paragraph before the table.
608
-
609
- | Name | Age |
610
- |---|---|
611
- | Alice | 30 |
612
-
613
- This is a paragraph after the table.`
614
-
615
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
616
- "
617
- This is a paragraph before the table.
618
- ┌─────┬───┐
619
- │Name │Age│
620
- ├─────┼───┤
621
- │Alice│30 │
622
- └─────┴───┘
623
-
624
- This is a paragraph after the table."
625
- `)
626
- })
627
-
628
- test("selection across markdown table includes table data", async () => {
629
- const markdown = `Intro line above table.
630
-
631
- | Component | Status | Notes |
632
- |---|---|---|
633
- | Authentication | **Done** | OAuth2 + SSO |
634
- | Payments API | *In Progress* | Retry + idempotency |
635
- | Search Indexer | \`Done\` | Ranking + typo fix |
636
-
637
- Outro line below table.`
638
-
639
- const md = createMarkdownRenderable({
640
- id: "markdown",
641
- content: markdown,
642
- syntaxStyle,
643
- })
644
-
645
- renderer.root.add(md)
646
- await renderMarkdownRenderable(md)
647
-
648
- const topBlock = md._blockStates[0]?.renderable as CodeRenderable | undefined
649
- const tableBlock = md._blockStates[1]?.renderable as TextTableRenderable | undefined
650
- const bottomBlock = md._blockStates[2]?.renderable as CodeRenderable | undefined
651
-
652
- expect(topBlock).toBeInstanceOf(CodeRenderable)
653
- expect(tableBlock).toBeInstanceOf(TextTableRenderable)
654
- expect(bottomBlock).toBeInstanceOf(CodeRenderable)
655
-
656
- const startX = topBlock!.x + 1
657
- const startY = topBlock!.y
658
- const endX = Math.max(bottomBlock!.x + bottomBlock!.width - 2, startX + 1)
659
- const endY = bottomBlock!.y
660
-
661
- await mockMouse.drag(startX, startY, endX, endY)
662
- await renderer.idle()
663
-
664
- const selectedText = renderer.getSelection()?.getSelectedText() ?? ""
665
-
666
- expect(selectedText).toContain("Authentication")
667
- expect(selectedText).toContain("Payments API")
668
- expect(selectedText).toContain("Retry + idempotency")
669
- })
670
-
671
- // Code block tests
672
-
673
- test("code block with language", async () => {
674
- const markdown = `\`\`\`typescript
675
- const x = 1;
676
- console.log(x);
677
- \`\`\``
678
-
679
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
680
- "
681
- const x = 1;
682
- console.log(x);"
683
- `)
684
- })
685
-
686
- test("code block without language", async () => {
687
- const markdown = `\`\`\`
688
- plain code block
689
- with multiple lines
690
- \`\`\``
691
-
692
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
693
- "
694
- plain code block
695
- with multiple lines"
696
- `)
697
- })
698
-
699
- test("code block mixed with text", async () => {
700
- const markdown = `Here is some code:
701
-
702
- \`\`\`js
703
- function hello() {
704
- return "world";
705
- }
706
- \`\`\`
707
-
708
- And here is more text after.`
709
-
710
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
711
- "
712
- Here is some code:
713
- function hello() {
714
- return "world";
715
- }
716
-
717
- And here is more text after."
718
- `)
719
- })
720
-
721
- test("multiple code blocks", async () => {
722
- const markdown = `First block:
723
-
724
- \`\`\`python
725
- print("hello")
726
- \`\`\`
727
-
728
- Second block:
729
-
730
- \`\`\`rust
731
- fn main() {}
732
- \`\`\``
733
-
734
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
735
- "
736
- First block:
737
- print("hello")
738
-
739
- Second block:
740
- fn main() {}"
741
- `)
742
- })
743
-
744
- test("code block in conceal=false mode", async () => {
745
- const markdown = `\`\`\`js
746
- const x = 1;
747
- \`\`\``
748
-
749
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
750
- "
751
- const x = 1;"
752
- `)
753
- })
754
-
755
- test("code block concealment is disabled by default", async () => {
756
- const mockTreeSitterClient = new MockTreeSitterClient()
757
- mockTreeSitterClient.setMockResult({
758
- highlights: [[0, 1, "conceal", { conceal: "" }]],
759
- })
760
-
761
- const md = createMarkdownRenderable({
762
- id: "markdown-code-default-conceal",
763
- content: "```markdown\n# Hidden heading\n```",
764
- syntaxStyle,
765
- conceal: true,
766
- treeSitterClient: mockTreeSitterClient,
767
- })
768
-
769
- renderer.root.add(md)
770
- await renderer.idle()
771
- expect(mockTreeSitterClient.isHighlighting()).toBe(true)
772
-
773
- mockTreeSitterClient.resolveAllHighlightOnce()
774
- await Bun.sleep(10)
775
- await renderer.idle()
776
-
777
- const frame = captureFrame()
778
- expect(frame).toContain("# Hidden heading")
779
- })
780
-
781
- test("code block concealment can be enabled with concealCode", async () => {
782
- const mockTreeSitterClient = new MockTreeSitterClient()
783
- mockTreeSitterClient.setMockResult({
784
- highlights: [[0, 1, "conceal", { conceal: "" }]],
785
- })
786
-
787
- const md = createMarkdownRenderable({
788
- id: "markdown-code-conceal-enabled",
789
- content: "```markdown\n# Hidden heading\n```",
790
- syntaxStyle,
791
- conceal: true,
792
- concealCode: true,
793
- treeSitterClient: mockTreeSitterClient,
794
- })
795
-
796
- renderer.root.add(md)
797
- await renderer.idle()
798
- expect(mockTreeSitterClient.isHighlighting()).toBe(true)
799
-
800
- mockTreeSitterClient.resolveAllHighlightOnce()
801
- await Bun.sleep(10)
802
- await renderer.idle()
803
-
804
- const frame = captureFrame()
805
- expect(frame).not.toContain("# Hidden heading")
806
- expect(frame).toContain("Hidden heading")
807
- })
808
-
809
- test("toggling concealCode updates existing code block renderables", async () => {
810
- const mockTreeSitterClient = new MockTreeSitterClient()
811
- mockTreeSitterClient.setMockResult({
812
- highlights: [[0, 1, "conceal", { conceal: "" }]],
813
- })
814
-
815
- const md = createMarkdownRenderable({
816
- id: "markdown-code-conceal-toggle",
817
- content: "```markdown\n# Hidden heading\n```",
818
- syntaxStyle,
819
- conceal: true,
820
- concealCode: false,
821
- treeSitterClient: mockTreeSitterClient,
822
- })
823
-
824
- renderer.root.add(md)
825
- await renderer.idle()
826
- expect(mockTreeSitterClient.isHighlighting()).toBe(true)
827
-
828
- mockTreeSitterClient.resolveAllHighlightOnce()
829
- await Bun.sleep(10)
830
- await renderer.idle()
831
-
832
- const frameBefore = captureFrame()
833
- expect(frameBefore).toContain("# Hidden heading")
834
-
835
- md.concealCode = true
836
- renderer.requestRender()
837
- await renderer.idle()
838
- expect(mockTreeSitterClient.isHighlighting()).toBe(true)
839
-
840
- mockTreeSitterClient.resolveAllHighlightOnce()
841
- await Bun.sleep(10)
842
- await renderer.idle()
843
-
844
- const frameAfter = captureFrame()
845
- expect(frameAfter).not.toContain("# Hidden heading")
846
- expect(frameAfter).toContain("Hidden heading")
847
- })
848
-
849
- // Heading tests
850
-
851
- test("headings h1 through h3", async () => {
852
- const markdown = `# Heading 1
853
-
854
- ## Heading 2
855
-
856
- ### Heading 3`
857
-
858
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
859
- "
860
- Heading 1
861
-
862
- Heading 2
863
-
864
- Heading 3"
865
- `)
866
- })
867
-
868
- test("headings with conceal=false show markers", async () => {
869
- const markdown = `# Heading 1
870
-
871
- ## Heading 2`
872
-
873
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
874
- "
875
- # Heading 1
876
-
877
- ## Heading 2"
878
- `)
879
- })
880
-
881
- // List tests
882
-
883
- test("unordered list", async () => {
884
- const markdown = `- Item one
885
- - Item two
886
- - Item three`
887
-
888
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
889
- "
890
- - Item one
891
- - Item two
892
- - Item three"
893
- `)
894
- })
895
-
896
- test("ordered list", async () => {
897
- const markdown = `1. First item
898
- 2. Second item
899
- 3. Third item`
900
-
901
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
902
- "
903
- 1. First item
904
- 2. Second item
905
- 3. Third item"
906
- `)
907
- })
908
-
909
- test("list with inline formatting", async () => {
910
- const markdown = `- **Bold** item
911
- - *Italic* item
912
- - \`Code\` item`
913
-
914
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
915
- "
916
- - Bold item
917
- - Italic item
918
- - Code item"
919
- `)
920
- })
921
-
922
- // Blockquote tests
923
-
924
- test("simple blockquote", async () => {
925
- const markdown = `> This is a quote
926
- > spanning multiple lines`
927
-
928
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
929
- "
930
- > This is a quote
931
- > spanning multiple lines"
932
- `)
933
- })
934
-
935
- // Inline formatting tests
936
-
937
- test("bold text", async () => {
938
- const markdown = `This has **bold** text in it.`
939
-
940
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
941
- "
942
- This has bold text in it."
943
- `)
944
- })
945
-
946
- test("italic text", async () => {
947
- const markdown = `This has *italic* text in it.`
948
-
949
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
950
- "
951
- This has italic text in it."
952
- `)
953
- })
954
-
955
- test("inline code", async () => {
956
- const markdown = `Use \`console.log()\` to debug.`
957
-
958
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
959
- "
960
- Use console.log() to debug."
961
- `)
962
- })
963
-
964
- test("mixed inline formatting", async () => {
965
- const markdown = `**Bold**, *italic*, and \`code\` together.`
966
-
967
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
968
- "
969
- Bold, italic, and code together."
970
- `)
971
- })
972
-
973
- test("inline formatting with conceal=false", async () => {
974
- const markdown = `**Bold**, *italic*, and \`code\` together.`
975
-
976
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
977
- "
978
- **Bold**, *italic*, and \`code\` together."
979
- `)
980
- })
981
-
982
- // Link tests
983
-
984
- test("links with conceal mode", async () => {
985
- const markdown = `Check out [OpenTUI](https://github.com/sst/opentui) for more.`
986
-
987
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
988
- "
989
- Check out OpenTUI (https://github.com/sst/opentui) for more."
990
- `)
991
- })
992
-
993
- test("links with conceal=false", async () => {
994
- const markdown = `Check out [OpenTUI](https://github.com/sst/opentui) for more.`
995
-
996
- expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`
997
- "
998
- Check out [OpenTUI](https://github.com/sst/opentui) for
999
- more."
1000
- `)
1001
- })
1002
-
1003
- // Horizontal rule
1004
-
1005
- test("horizontal rule", async () => {
1006
- const markdown = `Before
1007
-
1008
- ---
1009
-
1010
- After`
1011
-
1012
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1013
- "
1014
- Before
1015
-
1016
- ---
1017
-
1018
- After"
1019
- `)
1020
- })
1021
-
1022
- // Complex document
1023
-
1024
- test("complex markdown document", async () => {
1025
- const markdown = `# Project Title
1026
-
1027
- Welcome to **OpenTUI**, a terminal UI library.
1028
-
1029
- ## Features
1030
-
1031
- - Automatic table alignment
1032
- - \`inline code\` support
1033
- - *Italic* and **bold** text
1034
-
1035
- ## Code Example
1036
-
1037
- \`\`\`typescript
1038
- const md = new MarkdownRenderable(ctx, {
1039
- content: "# Hello",
1040
- })
1041
- \`\`\`
1042
-
1043
- ## Links
1044
-
1045
- Visit [GitHub](https://github.com) for more.
1046
-
1047
- ---
1048
-
1049
- *Press \`?\` for help*`
1050
-
1051
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1052
- "
1053
- Project Title
1054
-
1055
- Welcome to OpenTUI, a terminal UI library.
1056
-
1057
- Features
1058
-
1059
- - Automatic table alignment
1060
- - inline code support
1061
- - Italic and bold text
1062
-
1063
- Code Example
1064
-
1065
- const md = new MarkdownRenderable(ctx, {
1066
- content: "# Hello",
1067
- })
1068
-
1069
- Links
1070
-
1071
- Visit GitHub (https://github.com) for more.
1072
-
1073
- ---
1074
-
1075
- Press ? for help"
1076
- `)
1077
- })
1078
-
1079
- // Custom renderNode tests
1080
-
1081
- test("custom renderNode can override heading rendering", async () => {
1082
- const { TextRenderable } = await import("../Text")
1083
- const { StyledText } = await import("../../lib/styled-text")
1084
-
1085
- // Helper to extract text from marked tokens
1086
- const extractText = (node: any): string => {
1087
- if (node.type === "text") return node.text
1088
- if (node.tokens) return node.tokens.map(extractText).join("")
1089
- return ""
1090
- }
1091
-
1092
- const md = createMarkdownRenderable({
1093
- id: "custom-heading",
1094
- content: `# Custom Heading
1095
-
1096
- Regular paragraph.`,
1097
- syntaxStyle,
1098
- renderNode: (node, ctx) => {
1099
- if (node.type === "heading") {
1100
- const text = extractText(node)
1101
- return new TextRenderable(renderer, {
1102
- id: "custom",
1103
- content: new StyledText([{ __isChunk: true, text: `[CUSTOM] ${text}`, attributes: 0 }]),
1104
- width: "100%",
1105
- })
1106
- }
1107
- return ctx.defaultRender()
1108
- },
1109
- })
1110
-
1111
- renderer.root.add(md)
1112
- await renderMarkdownRenderable(md)
1113
-
1114
- const lines = captureFrame()
1115
- .split("\n")
1116
- .map((line) => line.trimEnd())
1117
- expect("\n" + lines.join("\n").trimEnd()).toMatchInlineSnapshot(`
1118
- "
1119
- [CUSTOM] Custom Heading
1120
- Regular paragraph."
1121
- `)
1122
- })
1123
-
1124
- test("custom renderNode can override code block rendering", async () => {
1125
- const { BoxRenderable } = await import("../Box")
1126
- const { TextRenderable } = await import("../Text")
1127
-
1128
- const md = createMarkdownRenderable({
1129
- id: "custom-code",
1130
- content: `\`\`\`js
1131
- const x = 1;
1132
- \`\`\``,
1133
- syntaxStyle,
1134
- renderNode: (node, ctx) => {
1135
- if (node.type === "code") {
1136
- const box = new BoxRenderable(renderer, {
1137
- id: "code-box",
1138
- border: true,
1139
- borderStyle: "single",
1140
- })
1141
- box.add(
1142
- new TextRenderable(renderer, {
1143
- id: "code-text",
1144
- content: `CODE: ${(node as any).text}`,
1145
- }),
1146
- )
1147
- return box
1148
- }
1149
- return ctx.defaultRender()
1150
- },
1151
- })
1152
-
1153
- renderer.root.add(md)
1154
- await renderMarkdownRenderable(md)
1155
-
1156
- const lines = captureFrame()
1157
- .split("\n")
1158
- .map((line) => line.trimEnd())
1159
- expect("\n" + lines.join("\n").trimEnd()).toMatchInlineSnapshot(`
1160
- "
1161
- ┌──────────────────────────────────────────────────────────┐
1162
- │CODE: const x = 1; │
1163
- └──────────────────────────────────────────────────────────┘"
1164
- `)
1165
- })
1166
-
1167
- test("custom renderNode returning null uses default", async () => {
1168
- const md = createMarkdownRenderable({
1169
- id: "custom-null",
1170
- content: `# Heading
1171
-
1172
- Paragraph text.`,
1173
- syntaxStyle,
1174
- renderNode: () => null,
1175
- })
1176
-
1177
- renderer.root.add(md)
1178
- await renderMarkdownRenderable(md)
1179
-
1180
- const lines = captureFrame()
1181
- .split("\n")
1182
- .map((line) => line.trimEnd())
1183
- expect("\n" + lines.join("\n").trimEnd()).toMatchInlineSnapshot(`
1184
- "
1185
- Heading
1186
-
1187
-
1188
- Paragraph text."
1189
- `)
1190
- })
1191
-
1192
- // Incomplete/invalid markdown tests
1193
-
1194
- test("incomplete code block (no closing fence)", async () => {
1195
- const markdown = `Here is some code:
1196
-
1197
- \`\`\`javascript
1198
- const x = 1;
1199
- console.log(x);`
1200
-
1201
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1202
- "
1203
- Here is some code:
1204
- const x = 1;
1205
- console.log(x);"
1206
- `)
1207
- })
1208
-
1209
- test("incomplete bold (no closing **)", async () => {
1210
- const markdown = `This has **unclosed bold text`
1211
-
1212
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1213
- "
1214
- This has **unclosed bold text"
1215
- `)
1216
- })
1217
-
1218
- test("incomplete italic (no closing *)", async () => {
1219
- const markdown = `This has *unclosed italic text`
1220
-
1221
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1222
- "
1223
- This has *unclosed italic text"
1224
- `)
1225
- })
1226
-
1227
- test("incomplete link (no closing paren)", async () => {
1228
- const markdown = `Check out [this link](https://example.com`
1229
-
1230
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1231
- "
1232
- Check out this link(https://example.com"
1233
- `)
1234
- })
1235
-
1236
- test("incomplete table (only header)", async () => {
1237
- const markdown = `| Header1 | Header2 |`
1238
-
1239
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1240
- "
1241
- | Header1 | Header2 |"
1242
- `)
1243
- })
1244
-
1245
- test("incomplete table (header + delimiter, no rows)", async () => {
1246
- const markdown = `| Header1 | Header2 |
1247
- |---|---|`
1248
-
1249
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1250
- "
1251
- | Header1 | Header2 |
1252
- |---|---|"
1253
- `)
1254
- })
1255
-
1256
- test("streaming-like content with partial code block", async () => {
1257
- const markdown = `# Title
1258
-
1259
- Some text before code.
1260
-
1261
- \`\`\`py`
1262
-
1263
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1264
- "
1265
- Title
1266
-
1267
- Some text before code."
1268
- `)
1269
- })
1270
-
1271
- test("malformed table with missing pipes", async () => {
1272
- const markdown = `| A | B
1273
- |---|---
1274
- | 1 | 2`
1275
-
1276
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1277
- "
1278
- ┌─┬─┐
1279
- │A│B│
1280
- ├─┼─┤
1281
- │1│2│
1282
- └─┴─┘"
1283
- `)
1284
- })
1285
-
1286
- test("trailing blank lines do not add spacing", async () => {
1287
- const markdown = `# Heading
1288
-
1289
- Paragraph text.
1290
-
1291
-
1292
- `
1293
-
1294
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1295
- "
1296
- Heading
1297
-
1298
- Paragraph text."
1299
- `)
1300
- })
1301
-
1302
- test("multiple trailing blank lines do not add spacing", async () => {
1303
- const markdown = `First paragraph.
1304
-
1305
- Second paragraph.
1306
-
1307
-
1308
-
1309
- `
1310
-
1311
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1312
- "
1313
- First paragraph.
1314
-
1315
- Second paragraph."
1316
- `)
1317
- })
1318
-
1319
- test("blank lines between blocks add spacing", async () => {
1320
- const markdown = `First
1321
-
1322
- Second
1323
-
1324
- Third`
1325
-
1326
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1327
- "
1328
- First
1329
-
1330
- Second
1331
-
1332
- Third"
1333
- `)
1334
- })
1335
-
1336
- test("code block at end with trailing blank lines", async () => {
1337
- const markdown = `Text before
1338
-
1339
- \`\`\`js
1340
- const x = 1;
1341
- \`\`\`
1342
-
1343
- `
1344
-
1345
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1346
- "
1347
- Text before
1348
- const x = 1;"
1349
- `)
1350
- })
1351
-
1352
- test("table at end with trailing blank lines", async () => {
1353
- const markdown = `| A | B |
1354
- |---|---|
1355
- | 1 | 2 |
1356
-
1357
-
1358
- `
1359
-
1360
- expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`
1361
- "
1362
- ┌─┬─┐
1363
- │A│B│
1364
- ├─┼─┤
1365
- │1│2│
1366
- └─┴─┘"
1367
- `)
1368
- })
1369
-
1370
- // Incremental parsing tests
1371
- test("incremental update reuses unchanged blocks when appending", async () => {
1372
- const md = createMarkdownRenderable({
1373
- id: "markdown",
1374
- content: "# Hello\n\nParagraph 1",
1375
- syntaxStyle,
1376
- streaming: true,
1377
- })
1378
-
1379
- renderer.root.add(md)
1380
- await renderer.idle()
1381
-
1382
- // Get reference to first block
1383
- const firstBlockBefore = md._blockStates[0]?.renderable
1384
-
1385
- // Append content
1386
- md.content = "# Hello\n\nParagraph 1\n\nParagraph 2"
1387
- await renderer.idle()
1388
-
1389
- // First block should be reused (same object reference)
1390
- const firstBlockAfter = md._blockStates[0]?.renderable
1391
- expect(firstBlockAfter).toBe(firstBlockBefore)
1392
- })
1393
-
1394
- test("streaming mode keeps trailing tokens unstable", async () => {
1395
- const md = createMarkdownRenderable({
1396
- id: "markdown",
1397
- content: "# Hello",
1398
- syntaxStyle,
1399
- streaming: true,
1400
- })
1401
-
1402
- renderer.root.add(md)
1403
- await renderMarkdownRenderable(md)
1404
-
1405
- const frame1 = captureFrame()
1406
- .split("\n")
1407
- .map((line) => line.trimEnd())
1408
- .join("\n")
1409
- .trimEnd()
1410
- expect(frame1).toContain("Hello")
1411
-
1412
- // Extend the heading
1413
- md.content = "# Hello World"
1414
- await renderMarkdownRenderable(md)
1415
-
1416
- const frame2 = captureFrame()
1417
- .split("\n")
1418
- .map((line) => line.trimEnd())
1419
- .join("\n")
1420
- .trimEnd()
1421
- expect(frame2).toContain("Hello World")
1422
- })
1423
-
1424
- test("streaming code blocks with concealCode=true do not flash unconcealed markdown", async () => {
1425
- const mockTreeSitterClient = new MockTreeSitterClient()
1426
- mockTreeSitterClient.setMockResult({
1427
- highlights: [[0, 1, "conceal", { conceal: "" }]],
1428
- })
1429
-
1430
- const recorder = new TestRecorder(renderer)
1431
- recorder.rec()
1432
-
1433
- const md = createMarkdownRenderable({
1434
- id: "markdown-streaming-conceal-flicker",
1435
- content: "# Stream\n\n```markdown\n# Hidden heading\n```",
1436
- syntaxStyle,
1437
- conceal: true,
1438
- concealCode: true,
1439
- streaming: true,
1440
- treeSitterClient: mockTreeSitterClient,
1441
- })
1442
-
1443
- renderer.root.add(md)
1444
- await renderer.idle()
1445
-
1446
- expect(mockTreeSitterClient.isHighlighting()).toBe(true)
1447
-
1448
- mockTreeSitterClient.resolveAllHighlightOnce()
1449
- await Bun.sleep(10)
1450
- await renderer.idle()
1451
-
1452
- recorder.stop()
1453
-
1454
- const frames = recorder.recordedFrames.map((frame) => frame.frame)
1455
- const unconcealedFrames = frames.filter((frame) => frame.includes("# Hidden heading"))
1456
- expect(unconcealedFrames.length).toBe(0)
1457
- })
1458
-
1459
- test("non-streaming mode parses all tokens as stable", async () => {
1460
- const md = createMarkdownRenderable({
1461
- id: "markdown",
1462
- content: "# Hello\n\nPara 1\n\nPara 2",
1463
- syntaxStyle,
1464
- streaming: false,
1465
- })
1466
-
1467
- renderer.root.add(md)
1468
- await renderer.idle()
1469
-
1470
- // Get parse state
1471
- const parseState = md._parseState
1472
- expect(parseState).not.toBeNull()
1473
- expect(parseState!.tokens.length).toBeGreaterThan(0)
1474
- })
1475
-
1476
- test("content update with same text does not rebuild", async () => {
1477
- const md = createMarkdownRenderable({
1478
- id: "markdown",
1479
- content: "# Hello",
1480
- syntaxStyle,
1481
- })
1482
-
1483
- renderer.root.add(md)
1484
- await renderer.idle()
1485
-
1486
- const blockBefore = md._blockStates[0]?.renderable
1487
-
1488
- // Set same content
1489
- md.content = "# Hello"
1490
- await renderer.idle()
1491
-
1492
- const blockAfter = md._blockStates[0]?.renderable
1493
- expect(blockAfter).toBe(blockBefore)
1494
- })
1495
-
1496
- test("block type change creates new renderable", async () => {
1497
- const md = createMarkdownRenderable({
1498
- id: "markdown",
1499
- content: "# Hello",
1500
- syntaxStyle,
1501
- })
1502
-
1503
- renderer.root.add(md)
1504
- await renderer.idle()
1505
-
1506
- const blockBefore = md._blockStates[0]?.renderable
1507
-
1508
- // Change from heading to paragraph
1509
- md.content = "Hello"
1510
- await renderer.idle()
1511
-
1512
- const blockAfter = md._blockStates[0]?.renderable
1513
- // Non-special markdown blocks are merged and reused as one markdown code renderable
1514
- expect(blockAfter).toBe(blockBefore)
1515
- })
1516
-
1517
- test("streaming property can be toggled", async () => {
1518
- const md = createMarkdownRenderable({
1519
- id: "markdown",
1520
- content: "# Hello",
1521
- syntaxStyle,
1522
- streaming: false,
1523
- })
1524
-
1525
- renderer.root.add(md)
1526
- await renderMarkdownRenderable(md)
1527
-
1528
- expect(md.streaming).toBe(false)
1529
- const blockBefore = md._blockStates[0]?.renderable
1530
-
1531
- md.streaming = true
1532
- expect(md.streaming).toBe(true)
1533
-
1534
- await renderMarkdownRenderable(md)
1535
-
1536
- const blockAfter = md._blockStates[0]?.renderable
1537
- expect(blockAfter).toBe(blockBefore)
1538
-
1539
- const frame = captureFrame()
1540
- .split("\n")
1541
- .map((line) => line.trimEnd())
1542
- .join("\n")
1543
- .trimEnd()
1544
- expect(frame).toContain("Hello")
1545
- })
1546
-
1547
- test("clearCache forces full rebuild", async () => {
1548
- const md = createMarkdownRenderable({
1549
- id: "markdown",
1550
- content: "# Hello\n\nWorld",
1551
- syntaxStyle,
1552
- })
1553
-
1554
- renderer.root.add(md)
1555
- await renderer.idle()
1556
-
1557
- const parseStateBefore = md._parseState
1558
-
1559
- md.clearCache()
1560
- await renderer.idle()
1561
-
1562
- const parseStateAfter = md._parseState
1563
- // Parse state should be different (was cleared and rebuilt)
1564
- expect(parseStateAfter).not.toBe(parseStateBefore)
1565
- })
1566
-
1567
- test("streaming->non-streaming transition keeps final table row visible", async () => {
1568
- const md = createMarkdownRenderable({
1569
- id: "markdown",
1570
- content: "| Value |\n|---|\n| first |\n| second |",
1571
- syntaxStyle,
1572
- streaming: true,
1573
- })
1574
-
1575
- renderer.root.add(md)
1576
- await renderer.idle()
1577
-
1578
- const tableWhileStreaming = md._blockStates[0]?.renderable
1579
-
1580
- let frame = captureFrame()
1581
- .split("\n")
1582
- .map((line) => line.trimEnd())
1583
- .join("\n")
1584
-
1585
- expect(frame).toContain("first")
1586
- expect(frame).toContain("second")
1587
-
1588
- md.streaming = false
1589
- await renderer.idle()
1590
-
1591
- frame = captureFrame()
1592
- .split("\n")
1593
- .map((line) => line.trimEnd())
1594
- .join("\n")
1595
-
1596
- expect(frame).toContain("first")
1597
- expect(frame).toContain("second")
1598
- expect(md._blockStates[0]?.renderable).toBe(tableWhileStreaming)
1599
- })
1600
-
1601
- test("streaming table remains visible when a new block starts", async () => {
1602
- const tableMarkdown = "| Value |\n|---|\n| first |\n| second |"
1603
- const md = createMarkdownRenderable({
1604
- id: "markdown",
1605
- content: tableMarkdown,
1606
- syntaxStyle,
1607
- streaming: true,
1608
- })
1609
-
1610
- renderer.root.add(md)
1611
- await renderer.idle()
1612
-
1613
- const tableWhileTrailing = md._blockStates[0]?.renderable
1614
-
1615
- let frame = captureFrame()
1616
- .split("\n")
1617
- .map((line) => line.trimEnd())
1618
- .join("\n")
1619
-
1620
- expect(frame).toContain("first")
1621
- expect(frame).toContain("second")
1622
-
1623
- md.content = `${tableMarkdown}\n\nAfter table block.`
1624
- await renderer.idle()
1625
-
1626
- frame = captureFrame()
1627
- .split("\n")
1628
- .map((line) => line.trimEnd())
1629
- .join("\n")
1630
-
1631
- expect(md.streaming).toBe(true)
1632
- expect(frame).toContain("first")
1633
- expect(frame).toContain("second")
1634
- expect(md._blockStates.length).toBeGreaterThan(1)
1635
- expect(md._blockStates[0]?.renderable).toBe(tableWhileTrailing)
1636
- })
1637
-
1638
- test("stream end mid-table finalizes full table snapshot", async () => {
1639
- const md = createMarkdownRenderable({
1640
- id: "markdown",
1641
- content: "",
1642
- syntaxStyle,
1643
- streaming: true,
1644
- })
1645
-
1646
- renderer.root.add(md)
1647
-
1648
- md.content = "| Name | Score |\n|---|---|\n"
1649
- await renderer.idle()
1650
-
1651
- md.content = "| Name | Score |\n|---|---|\n| Alpha | 10 |\n"
1652
- await renderer.idle()
1653
-
1654
- md.content = "| Name | Score |\n|---|---|\n| Alpha | 10 |\n| Bravo | 20 |\n"
1655
- await renderer.idle()
1656
-
1657
- md.content = "| Name | Score |\n|---|---|\n| Alpha | 10 |\n| Bravo | 20 |\n| Charlie | 30 |"
1658
- await renderer.idle()
1659
-
1660
- let frame = captureFrame()
1661
- .split("\n")
1662
- .map((line) => line.trimEnd())
1663
- .join("\n")
1664
-
1665
- expect(frame).toContain("Charlie")
1666
-
1667
- md.streaming = false
1668
- await renderer.idle()
1669
-
1670
- frame = captureFrame()
1671
- .split("\n")
1672
- .map((line) => line.trimEnd())
1673
- .join("\n")
1674
- .trimEnd()
1675
-
1676
- expect(frame).toMatchInlineSnapshot(`
1677
- "┌──────────────────────────────┬───────────────────────────┐
1678
- │Name │Score │
1679
- ├──────────────────────────────┼───────────────────────────┤
1680
- │Alpha │10 │
1681
- ├──────────────────────────────┼───────────────────────────┤
1682
- │Bravo │20 │
1683
- ├──────────────────────────────┼───────────────────────────┤
1684
- │Charlie │30 │
1685
- └──────────────────────────────┴───────────────────────────┘"
1686
- `)
1687
- })
1688
-
1689
- test("ignores content updates after markdown renderable is destroyed during streaming", async () => {
1690
- const md = createMarkdownRenderable({
1691
- id: "markdown",
1692
- content: "",
1693
- syntaxStyle,
1694
- streaming: true,
1695
- })
1696
-
1697
- renderer.root.add(md)
1698
-
1699
- md.content = "| Name | Score |\n|---|---|\n| Alpha | 10 |\n"
1700
- await renderer.idle()
1701
-
1702
- md.destroyRecursively()
1703
- expect(md.isDestroyed).toBe(true)
1704
-
1705
- expect(() => {
1706
- md.content = "| Name | Score |\n|---|---|\n| Alpha | 10 |\n| Bravo | 20 |\n"
1707
- md.streaming = false
1708
- }).not.toThrow()
1709
-
1710
- await renderer.idle()
1711
- })
1712
-
1713
- test("non-streaming->streaming transition keeps final table row visible", async () => {
1714
- const md = createMarkdownRenderable({
1715
- id: "markdown",
1716
- content: "| Value |\n|---|\n| first |\n| second |",
1717
- syntaxStyle,
1718
- streaming: false,
1719
- })
1720
-
1721
- renderer.root.add(md)
1722
- await renderer.idle()
1723
-
1724
- const tableWhileStable = md._blockStates[0]?.renderable
1725
-
1726
- let frame = captureFrame()
1727
- .split("\n")
1728
- .map((line) => line.trimEnd())
1729
- .join("\n")
1730
-
1731
- expect(frame).toContain("first")
1732
- expect(frame).toContain("second")
1733
-
1734
- md.streaming = true
1735
- await renderer.idle()
1736
-
1737
- frame = captureFrame()
1738
- .split("\n")
1739
- .map((line) => line.trimEnd())
1740
- .join("\n")
1741
-
1742
- expect(frame).toContain("first")
1743
- expect(frame).toContain("second")
1744
- expect(md._blockStates[0]?.renderable).toBe(tableWhileStable)
1745
- })
1746
-
1747
- test("streaming table reuses renderable while updating row content", async () => {
1748
- const md = createMarkdownRenderable({
1749
- id: "markdown",
1750
- content: "| A |\n|---|\n| 1 |",
1751
- syntaxStyle,
1752
- streaming: true,
1753
- })
1754
-
1755
- renderer.root.add(md)
1756
- await renderer.idle()
1757
-
1758
- const tableBefore = md._blockStates[0]?.renderable
1759
-
1760
- md.content = "| B |\n|---|\n| 2 |"
1761
- await renderer.idle()
1762
-
1763
- const tableAfterSameRows = md._blockStates[0]?.renderable
1764
- expect(tableAfterSameRows).toBe(tableBefore)
1765
-
1766
- md.content = "| B |\n|---|\n| 2 |\n| 3 |"
1767
- await renderer.idle()
1768
-
1769
- const tableAfterNewRow = md._blockStates[0]?.renderable
1770
- expect(tableAfterNewRow).toBe(tableBefore)
1771
- })
1772
-
1773
- test("table shows all rows when streaming is false", async () => {
1774
- const md = createMarkdownRenderable({
1775
- id: "markdown",
1776
- content: "| A |\n|---|\n| 1 |",
1777
- syntaxStyle,
1778
- streaming: false,
1779
- })
1780
-
1781
- renderer.root.add(md)
1782
- await renderer.idle()
1783
-
1784
- // Non-streaming should show all rows including the last
1785
- const frame = captureFrame()
1786
- .split("\n")
1787
- .map((line) => line.trimEnd())
1788
- .join("\n")
1789
- expect(frame).toContain("1")
1790
- })
1791
-
1792
- test("table updates content when not streaming", async () => {
1793
- const md = createMarkdownRenderable({
1794
- id: "markdown",
1795
- content: "| A |\n|---|\n| 1 |",
1796
- syntaxStyle,
1797
- streaming: false,
1798
- })
1799
-
1800
- renderer.root.add(md)
1801
- await renderer.idle()
1802
-
1803
- const frame1 = captureFrame()
1804
- expect(frame1).toContain("1")
1805
-
1806
- // Change cell content - should update immediately when not streaming
1807
- md.content = "| A |\n|---|\n| 2 |"
1808
- await renderer.idle()
1809
-
1810
- const frame2 = captureFrame()
1811
- expect(frame2).toContain("2")
1812
- expect(frame2).not.toContain("1")
1813
- })
1814
-
1815
- test("table keeps unchanged cell chunks stable across updates", async () => {
1816
- const md = createMarkdownRenderable({
1817
- id: "markdown",
1818
- content: "| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |",
1819
- syntaxStyle,
1820
- streaming: false,
1821
- })
1822
-
1823
- renderer.root.add(md)
1824
- await renderer.idle()
1825
-
1826
- const table = md._blockStates[0]?.renderable as TextTableRenderable
1827
- expect(table).toBeInstanceOf(TextTableRenderable)
1828
-
1829
- const headerBefore = table.content[0]?.[0]
1830
- const firstRowBefore = table.content[1]?.[0]
1831
- const secondRowSecondCellBefore = table.content[2]?.[1]
1832
- const changedCellBefore = table.content[2]?.[0]
1833
-
1834
- md.content = "| A | B |\n|---|---|\n| 1 | 2 |\n| 33 | 4 |"
1835
- await renderer.idle()
1836
-
1837
- const tableAfter = md._blockStates[0]?.renderable as TextTableRenderable
1838
- expect(tableAfter).toBe(table)
1839
- expect(tableAfter.content[0]?.[0]).toBe(headerBefore)
1840
- expect(tableAfter.content[1]?.[0]).toBe(firstRowBefore)
1841
- expect(tableAfter.content[2]?.[1]).toBe(secondRowSecondCellBefore)
1842
- expect(tableAfter.content[2]?.[0]).not.toBe(changedCellBefore)
1843
- })
1844
-
1845
- test("streaming table updates trailing row content", async () => {
1846
- const md = createMarkdownRenderable({
1847
- id: "markdown",
1848
- content: "| A |\n|---|\n| 1 |\n| 2 |",
1849
- syntaxStyle,
1850
- streaming: true,
1851
- })
1852
-
1853
- renderer.root.add(md)
1854
- await renderer.idle()
1855
-
1856
- const table = md._blockStates[0]?.renderable as TextTableRenderable
1857
- const contentBefore = table.content
1858
-
1859
- md.content = "| A |\n|---|\n| 1 |\n| 200 |"
1860
- await renderer.idle()
1861
-
1862
- const tableAfter = md._blockStates[0]?.renderable as TextTableRenderable
1863
- const frame = captureFrame()
1864
- expect(tableAfter).toBe(table)
1865
- expect(tableAfter.content).not.toBe(contentBefore)
1866
- expect(frame).toContain("200")
1867
- })
1868
-
1869
- test("streaming complex tables keep final rows visible (issue #15244)", async () => {
1870
- const vmHeader = "| VM | 状态 | Owner | Zone | CPU | Mem(GB) | Disk(GB) | Net | Uptime | Cost/月 | Notes |"
1871
- const vmDelimiter = "|---|---|---|---|---|---|---|---|---|---|---|"
1872
- const vmRows = [
1873
- "| vm-api-01 | 🟢 运行中 | alice | us-east-1a | 8 | 32 | 500 | 1.2Gbps | 99.99% | 12,345 | 主节点 — steady |",
1874
- "| vm-job-02 | 🟢 运行中 | bob | ap-south-1b | 16 | 64 | 1,024 | 950Mbps | 98.70% | 23,456 | 批处理 — spikes |",
1875
- "| vm-batch-03 | 🟡 维护中 | carol | eu-west-1c | 32 | 128 | 2,048 | 2.4Gbps | 97.10% | 34,567 | 最后一行 — must stay |",
1876
- ] as const
1877
-
1878
- const storageHeader = "| 存储池 | 状态 | 使用率 | 可用(GB) | 已用(GB) | 冗余 | 备注 |"
1879
- const storageDelimiter = "|---|---|---|---|---|---|---|"
1880
- const storageRows = [
1881
- "| 热池A | 🟢 正常 | 72% | 12,500 | 32,500 | 3x | 混合负载 |",
1882
- "| 温池B | 🟢 正常 | 81% | 8,250 | 35,750 | 2x | 历史数据 |",
1883
- "| 冷池C | 🟡 告警 | 93% | 2,100 | 27,900 | 2x | 最后一行 — must stay |",
1884
- ] as const
1885
-
1886
- const buildContent = (vmRowCount: number, storageRowCount: number): string =>
1887
- `### VM details\n\n${vmHeader}\n${vmDelimiter}\n${vmRows.slice(0, vmRowCount).join("\n")}\n\n### Storage details\n\n${storageHeader}\n${storageDelimiter}\n${storageRows.slice(0, storageRowCount).join("\n")}`
1888
-
1889
- const md = createMarkdownRenderable({
1890
- id: "markdown",
1891
- content: "",
1892
- syntaxStyle,
1893
- streaming: true,
1894
- })
1895
-
1896
- renderer.root.add(md)
1897
-
1898
- for (const [vmRowCount, storageRowCount] of [
1899
- [2, 2],
1900
- [3, 2],
1901
- [3, 3],
1902
- ] as const) {
1903
- md.content = buildContent(vmRowCount, storageRowCount)
1904
- await renderMarkdownRenderable(md)
1905
- }
1906
-
1907
- const tableBlocks = md._blockStates
1908
- .map((state) => state.renderable)
1909
- .filter((renderable): renderable is TextTableRenderable => renderable instanceof TextTableRenderable)
1910
-
1911
- const cellText = (cell: { text: string }[] | null | undefined): string =>
1912
- cell?.map((chunk) => chunk.text).join("") ?? ""
1913
-
1914
- expect(tableBlocks).toHaveLength(2)
1915
-
1916
- const vmTable = tableBlocks[0]
1917
- const storageTable = tableBlocks[1]
1918
-
1919
- expect(vmTable.content.length).toBe(4)
1920
- expect(storageTable.content.length).toBe(4)
1921
- expect(cellText(vmTable.content[3]?.[0])).toContain("vm-batch-03")
1922
- expect(cellText(storageTable.content[3]?.[0])).toContain("冷池C")
1923
- })
1924
-
1925
- test("streaming table with incomplete first row is rendered with padded cells", async () => {
1926
- const md = createMarkdownRenderable({
1927
- id: "markdown",
1928
- content: "| A |\n|---|\n|",
1929
- syntaxStyle,
1930
- streaming: true,
1931
- })
1932
-
1933
- renderer.root.add(md)
1934
- await renderMarkdownRenderable(md)
1935
-
1936
- const frame1 = captureFrame()
1937
- .split("\n")
1938
- .map((line) => line.trimEnd())
1939
- .join("\n")
1940
-
1941
- expect(frame1).toMatch(/[┌│└]/)
1942
- expect(frame1).toContain("A")
1943
-
1944
- md.content = "| A |\n|---|\n| 1"
1945
- await renderMarkdownRenderable(md)
1946
-
1947
- const frame2 = captureFrame()
1948
- .split("\n")
1949
- .map((line) => line.trimEnd())
1950
- .join("\n")
1951
-
1952
- expect(frame2).toMatch(/[┌│└]/)
1953
- expect(frame2).toContain("1")
1954
-
1955
- md.content = "| A |\n|---|\n| 1 |\n| 2 |"
1956
- await renderMarkdownRenderable(md)
1957
-
1958
- const frame3 = captureFrame()
1959
- .split("\n")
1960
- .map((line) => line.trimEnd())
1961
- .join("\n")
1962
-
1963
- expect(frame3).toMatch(/[┌│└]/)
1964
- expect(frame3).toContain("1")
1965
- expect(frame3).toContain("2")
1966
- })
1967
-
1968
- test("streaming table transitions from raw text to table once first row appears", async () => {
1969
- const md = createMarkdownRenderable({
1970
- id: "markdown",
1971
- content: "| Header |",
1972
- syntaxStyle,
1973
- streaming: true,
1974
- })
1975
-
1976
- renderer.root.add(md)
1977
- await renderMarkdownRenderable(md)
1978
-
1979
- let frame = captureFrame()
1980
- .split("\n")
1981
- .map((line) => line.trimEnd())
1982
- .join("\n")
1983
- expect(frame).toContain("| Header |")
1984
- expect(frame).not.toMatch(/[┌│└]/)
1985
-
1986
- md.content = "| Header |\n|---|"
1987
- await renderMarkdownRenderable(md)
1988
-
1989
- frame = captureFrame()
1990
- .split("\n")
1991
- .map((line) => line.trimEnd())
1992
- .join("\n")
1993
- expect(frame).toContain("|---|")
1994
- expect(frame).not.toMatch(/[┌│└]/)
1995
-
1996
- md.content = "| Header |\n|---|\n| D"
1997
- await renderMarkdownRenderable(md)
1998
-
1999
- frame = captureFrame()
2000
- .split("\n")
2001
- .map((line) => line.trimEnd())
2002
- .join("\n")
2003
- expect(frame).toMatch(/[┌│└]/)
2004
- expect(frame).toContain("Header")
2005
- expect(frame).toContain("D")
2006
- expect(frame).not.toContain("|---|")
2007
- })
2008
-
2009
- test("streaming table remains rendered when row count decreases", async () => {
2010
- const md = createMarkdownRenderable({
2011
- id: "markdown",
2012
- content: "| A |\n|---|\n| 1 |\n| 2 |",
2013
- syntaxStyle,
2014
- streaming: true,
2015
- })
2016
-
2017
- renderer.root.add(md)
2018
- await renderMarkdownRenderable(md)
2019
-
2020
- let frame = captureFrame()
2021
- .split("\n")
2022
- .map((line) => line.trimEnd())
2023
- .join("\n")
2024
- expect(frame).toMatch(/[┌│└]/)
2025
- expect(frame).toContain("1")
2026
- expect(frame).toContain("2")
2027
-
2028
- md.content = "| A |\n|---|\n| 1 |"
2029
- await renderMarkdownRenderable(md)
2030
-
2031
- frame = captureFrame()
2032
- .split("\n")
2033
- .map((line) => line.trimEnd())
2034
- .join("\n")
2035
- expect(frame).toMatch(/[┌│└]/)
2036
- expect(frame).toContain("1")
2037
- expect(frame).not.toContain("|---|")
2038
- })
2039
-
2040
- test("conceal change updates rendered content", async () => {
2041
- const md = createMarkdownRenderable({
2042
- id: "markdown",
2043
- content: "# Hello **bold**",
2044
- syntaxStyle,
2045
- conceal: true,
2046
- })
2047
-
2048
- renderer.root.add(md)
2049
- await renderMarkdownRenderable(md)
2050
-
2051
- const frame1 = captureFrame()
2052
- expect(frame1).not.toContain("**")
2053
- expect(frame1).not.toContain("#")
2054
-
2055
- md.conceal = false
2056
- renderer.requestRender()
2057
- await renderMarkdownRenderable(md)
2058
-
2059
- const frame2 = captureFrame()
2060
- expect(frame2).toContain("**")
2061
- expect(frame2).toContain("#")
2062
- })
2063
-
2064
- test("theme switching (syntaxStyle change)", async () => {
2065
- const theme1 = SyntaxStyle.fromStyles({
2066
- default: { fg: RGBA.fromValues(1, 0, 0, 1) }, // Red
2067
- "markup.heading.1": { fg: RGBA.fromValues(0, 1, 0, 1), bold: true }, // Green
2068
- })
2069
-
2070
- const theme2 = SyntaxStyle.fromStyles({
2071
- default: { fg: RGBA.fromValues(0, 0, 1, 1) }, // Blue
2072
- "markup.heading.1": { fg: RGBA.fromValues(1, 1, 0, 1), bold: true }, // Yellow
2073
- })
2074
-
2075
- // Use the EXACT content from markdown-demo.ts to reproduce the issue
2076
- const content = `# OpenTUI Markdown Demo
2077
-
2078
- Welcome to the **MarkdownRenderable** showcase! This demonstrates automatic table alignment and syntax highlighting.
2079
-
2080
- ## Features
2081
-
2082
- - Automatic **table column alignment** based on content width
2083
- - Proper handling of \`inline code\`, **bold**, and *italic* in tables
2084
- - Multiple syntax themes to choose from
2085
- - Conceal mode hides formatting markers
2086
-
2087
- ## Comparison Table
2088
-
2089
- | Feature | Status | Priority | Notes |
2090
- |---|---|---|---|
2091
- | Table alignment | **Done** | High | Uses \`marked\` parser |
2092
- | Conceal mode | *Working* | Medium | Hides \`**\`, \`\`\`, etc. |
2093
- | Theme switching | **Done** | Low | 3 themes available |
2094
- | Unicode support | 日本語 | High | CJK characters |
2095
-
2096
- ## Code Examples
2097
-
2098
- Here's how to use it:
2099
-
2100
- \`\`\`typescript
2101
- import { MarkdownRenderable } from "@opentui/core"
2102
-
2103
- const md = createMarkdownRenderable({
2104
- content: "# Hello World",
2105
- syntaxStyle: mySyntaxStyle,
2106
- conceal: true, // Hide formatting markers
2107
- })
2108
- \`\`\`
2109
-
2110
- ### API Reference
2111
-
2112
- | Method | Parameters | Returns | Description |
2113
- |---|---|---|---|
2114
- | \`constructor\` | \`ctx, options\` | \`MarkdownRenderable\` | Create new instance |
2115
- | \`clearCache\` | none | \`void\` | Force re-render content |
2116
-
2117
- ## Inline Formatting Examples
2118
-
2119
- | Style | Syntax | Rendered |
2120
- |---|---|---|
2121
- | Bold | \`**text**\` | **bold text** |
2122
- | Italic | \`*text*\` | *italic text* |
2123
- | Code | \`code\` | \`inline code\` |
2124
- | Link | \`[text](url)\` | [OpenTUI](https://github.com) |
2125
-
2126
- ## Mixed Content
2127
-
2128
- > **Note**: This blockquote contains **bold** and \`code\` formatting.
2129
- > It should render correctly with proper styling.
2130
-
2131
- ### Emoji Support
2132
-
2133
- | Emoji | Name | Category |
2134
- |---|---|---|
2135
- | 🚀 | Rocket | Transport |
2136
- | 🎨 | Palette | Art |
2137
- | ⚡ | Lightning | Nature |
2138
- | 🔥 | Fire | Nature |
2139
-
2140
- ---
2141
-
2142
- ## Alignment Examples
2143
-
2144
- | Left | Center | Right |
2145
- |:---|:---:|---:|
2146
- | L1 | C1 | R1 |
2147
- | Left aligned | Centered text | Right aligned |
2148
- | Short | Medium length | Longer content here |
2149
-
2150
- ## Performance
2151
-
2152
- The table alignment uses:
2153
- 1. AST-based parsing with \`marked\`
2154
- 2. Caching for repeated content
2155
- 3. Smart width calculation accounting for concealed chars
2156
-
2157
- ---
2158
-
2159
- *Press \`?\` for keybindings*
2160
- `
2161
-
2162
- const md = createMarkdownRenderable({
2163
- id: "markdown",
2164
- content,
2165
- syntaxStyle: theme1,
2166
- conceal: true,
2167
- })
2168
-
2169
- renderer.root.add(md)
2170
- await renderMarkdownRenderable(md)
2171
-
2172
- const findSpanContaining = (frame: CapturedFrame, text: string) => {
2173
- for (const line of frame.lines) {
2174
- const span = line.spans.find((candidate) => candidate.text.includes(text))
2175
- if (span) return span
2176
- }
2177
- return undefined
2178
- }
2179
-
2180
- const frame1 = captureSpans()
2181
- const headingSpan1 = findSpanContaining(frame1, "OpenTUI Markdown Demo")
2182
- expect(headingSpan1).toBeDefined()
2183
- expect(headingSpan1!.fg.r).toBe(0)
2184
- expect(headingSpan1!.fg.g).toBe(1)
2185
- expect(headingSpan1!.fg.b).toBe(0)
2186
- expect(headingSpan1!.attributes & TextAttributes.BOLD).toBeTruthy()
2187
-
2188
- // Switch theme
2189
- md.syntaxStyle = theme2
2190
- renderer.requestRender()
2191
- await renderMarkdownRenderable(md)
2192
-
2193
- const frame2 = captureSpans()
2194
- const headingSpan2 = findSpanContaining(frame2, "OpenTUI Markdown Demo")
2195
- expect(headingSpan2).toBeDefined()
2196
- expect(headingSpan2!.fg.r).toBe(1)
2197
- expect(headingSpan2!.fg.g).toBe(1)
2198
- expect(headingSpan2!.fg.b).toBe(0)
2199
- expect(headingSpan2!.attributes & TextAttributes.BOLD).toBeTruthy()
2200
- })
2201
-
2202
- // Paragraph rendering tests
2203
-
2204
- test("paragraph links are rendered with markdown conceal behavior", async () => {
2205
- const md = createMarkdownRenderable({
2206
- id: "markdown",
2207
- content: "Check [Google](https://google.com) out",
2208
- syntaxStyle,
2209
- conceal: true,
2210
- })
2211
-
2212
- renderer.root.add(md)
2213
- await renderMarkdownRenderable(md)
2214
-
2215
- const paragraphChildren = md.getChildren()
2216
- expect(paragraphChildren.length).toBe(1)
2217
- expect(paragraphChildren[0]).toBeInstanceOf(CodeRenderable)
2218
- expect(paragraphChildren[0]).not.toBeInstanceOf(TextRenderable)
2219
-
2220
- const frame = captureFrame()
2221
- expect(frame).toContain("Google")
2222
- expect(frame).toContain("https://google.com")
2223
- expect(frame).not.toContain("[Google](https://google.com)")
2224
- })
2225
-
2226
- test("paragraph initial render does not flash raw markdown markers", async () => {
2227
- const recorder = new TestRecorder(renderer)
2228
- recorder.rec()
2229
-
2230
- const md = createMarkdownRenderable({
2231
- id: "markdown",
2232
- content: "This has **bold** text.",
2233
- syntaxStyle,
2234
- conceal: true,
2235
- })
2236
-
2237
- renderer.root.add(md)
2238
- await renderMarkdownRenderable(md)
2239
- recorder.stop()
2240
-
2241
- const paragraphChildren = md.getChildren()
2242
- expect(paragraphChildren.length).toBe(1)
2243
- expect(paragraphChildren[0]).toBeInstanceOf(CodeRenderable)
2244
- expect(paragraphChildren[0]).not.toBeInstanceOf(TextRenderable)
2245
-
2246
- const rawMarkdownFrames = recorder.recordedFrames.filter((recorded) => recorded.frame.includes("**bold**"))
2247
- expect(rawMarkdownFrames.length).toBe(0)
2248
-
2249
- const finalFrame = captureFrame()
2250
- expect(finalFrame).toContain("This has bold text.")
2251
- })
2252
-
2253
- test("paragraph updates do not flash raw markdown markers", async () => {
2254
- const md = createMarkdownRenderable({
2255
- id: "markdown",
2256
- content: "**First** value",
2257
- syntaxStyle,
2258
- conceal: true,
2259
- })
2260
-
2261
- renderer.root.add(md)
2262
- await renderMarkdownRenderable(md)
2263
-
2264
- const paragraphChildrenBefore = md.getChildren()
2265
- expect(paragraphChildrenBefore.length).toBe(1)
2266
- expect(paragraphChildrenBefore[0]).toBeInstanceOf(CodeRenderable)
2267
- expect(paragraphChildrenBefore[0]).not.toBeInstanceOf(TextRenderable)
2268
-
2269
- const recorder = new TestRecorder(renderer)
2270
- recorder.rec()
2271
-
2272
- md.content = "**Second** value"
2273
- await renderMarkdownRenderable(md)
2274
- recorder.stop()
2275
-
2276
- const paragraphChildrenAfter = md.getChildren()
2277
- expect(paragraphChildrenAfter.length).toBe(1)
2278
- expect(paragraphChildrenAfter[0]).toBeInstanceOf(CodeRenderable)
2279
- expect(paragraphChildrenAfter[0]).not.toBeInstanceOf(TextRenderable)
2280
-
2281
- const rawMarkdownFrames = recorder.recordedFrames.filter((recorded) => recorded.frame.includes("**Second**"))
2282
- expect(rawMarkdownFrames.length).toBe(0)
2283
-
2284
- const finalFrame = captureFrame()
2285
- expect(finalFrame).toContain("Second value")
2286
- expect(finalFrame).not.toContain("**Second**")
2287
- })