@fairyhunter13/opentui-core 0.1.114 → 0.1.115

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