@fairyhunter13/opentui-core 0.1.113 → 0.1.114
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.
- package/dev/keypress-debug-renderer.ts +148 -0
- package/dev/keypress-debug.ts +43 -0
- package/dev/print-env-vars.ts +32 -0
- package/dev/test-tmux-graphics-334.sh +68 -0
- package/dev/thai-debug-test.ts +68 -0
- package/docs/development.md +144 -0
- package/package.json +62 -53
- package/scripts/build.ts +400 -0
- package/scripts/publish.ts +60 -0
- package/src/3d/SpriteResourceManager.ts +286 -0
- package/src/3d/SpriteUtils.ts +70 -0
- package/src/3d/TextureUtils.ts +196 -0
- package/src/3d/ThreeRenderable.ts +197 -0
- package/src/3d/WGPURenderer.ts +294 -0
- package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
- package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
- package/src/3d/animation/SpriteAnimator.ts +633 -0
- package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
- package/src/3d/canvas.ts +464 -0
- package/src/3d/index.ts +12 -0
- package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
- package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
- package/src/3d/physics/physics-interface.ts +31 -0
- package/src/3d/shaders/supersampling.wgsl +201 -0
- package/src/3d.ts +3 -0
- package/src/NativeSpanFeed.ts +300 -0
- package/src/Renderable.ts +1704 -0
- package/src/__snapshots__/buffer.test.ts.snap +28 -0
- package/src/animation/Timeline.test.ts +2709 -0
- package/src/animation/Timeline.ts +598 -0
- package/src/ansi.ts +18 -0
- package/src/benchmark/attenuation-benchmark.ts +81 -0
- package/src/benchmark/colormatrix-benchmark.ts +128 -0
- package/src/benchmark/gain-benchmark.ts +80 -0
- package/src/benchmark/latest-all-bench-run.json +707 -0
- package/src/benchmark/latest-async-bench-run.json +336 -0
- package/src/benchmark/latest-default-bench-run.json +657 -0
- package/src/benchmark/latest-large-bench-run.json +707 -0
- package/src/benchmark/latest-quick-bench-run.json +207 -0
- package/src/benchmark/markdown-benchmark.ts +1796 -0
- package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
- package/src/benchmark/native-span-feed-benchmark.md +56 -0
- package/src/benchmark/native-span-feed-benchmark.ts +596 -0
- package/src/benchmark/native-span-feed-compare.ts +280 -0
- package/src/benchmark/renderer-benchmark.ts +754 -0
- package/src/benchmark/text-table-benchmark.ts +948 -0
- package/src/buffer.test.ts +291 -0
- package/src/buffer.ts +554 -0
- package/src/console.test.ts +612 -0
- package/src/console.ts +1254 -0
- package/src/edit-buffer.test.ts +1769 -0
- package/src/edit-buffer.ts +411 -0
- package/src/editor-view.test.ts +1032 -0
- package/src/editor-view.ts +284 -0
- package/src/examples/ascii-font-selection-demo.ts +245 -0
- package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
- package/src/examples/assets/concrete.png +0 -0
- package/src/examples/assets/crate.png +0 -0
- package/src/examples/assets/crate_emissive.png +0 -0
- package/src/examples/assets/forrest_background.png +0 -0
- package/src/examples/assets/hast-example.json +1018 -0
- package/src/examples/assets/heart.png +0 -0
- package/src/examples/assets/main_char_heavy_attack.png +0 -0
- package/src/examples/assets/main_char_idle.png +0 -0
- package/src/examples/assets/main_char_jump_end.png +0 -0
- package/src/examples/assets/main_char_jump_landing.png +0 -0
- package/src/examples/assets/main_char_jump_start.png +0 -0
- package/src/examples/assets/main_char_run_loop.png +0 -0
- package/src/examples/assets/roughness_map.jpg +0 -0
- package/src/examples/build.ts +115 -0
- package/src/examples/code-demo.ts +924 -0
- package/src/examples/console-demo.ts +358 -0
- package/src/examples/core-plugin-slots-demo.ts +759 -0
- package/src/examples/diff-demo.ts +701 -0
- package/src/examples/draggable-three-demo.ts +259 -0
- package/src/examples/editor-demo.ts +322 -0
- package/src/examples/extmarks-demo.ts +196 -0
- package/src/examples/focus-restore-demo.ts +310 -0
- package/src/examples/fonts.ts +245 -0
- package/src/examples/fractal-shader-demo.ts +268 -0
- package/src/examples/framebuffer-demo.ts +674 -0
- package/src/examples/full-unicode-demo.ts +241 -0
- package/src/examples/golden-star-demo.ts +933 -0
- package/src/examples/grayscale-buffer-demo.ts +249 -0
- package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
- package/src/examples/index.ts +926 -0
- package/src/examples/input-demo.ts +377 -0
- package/src/examples/input-select-layout-demo.ts +425 -0
- package/src/examples/install.sh +143 -0
- package/src/examples/keypress-debug-demo.ts +452 -0
- package/src/examples/lib/HexList.ts +122 -0
- package/src/examples/lib/PaletteGrid.ts +125 -0
- package/src/examples/lib/standalone-keys.ts +25 -0
- package/src/examples/lib/tab-controller.ts +243 -0
- package/src/examples/lights-phong-demo.ts +290 -0
- package/src/examples/link-demo.ts +220 -0
- package/src/examples/live-state-demo.ts +480 -0
- package/src/examples/markdown-demo.ts +725 -0
- package/src/examples/mouse-interaction-demo.ts +428 -0
- package/src/examples/nested-zindex-demo.ts +357 -0
- package/src/examples/opacity-example.ts +235 -0
- package/src/examples/opentui-demo.ts +1057 -0
- package/src/examples/physx-planck-2d-demo.ts +623 -0
- package/src/examples/physx-rapier-2d-demo.ts +655 -0
- package/src/examples/relative-positioning-demo.ts +323 -0
- package/src/examples/scroll-example.ts +214 -0
- package/src/examples/scrollbox-mouse-test.ts +112 -0
- package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
- package/src/examples/select-demo.ts +237 -0
- package/src/examples/shader-cube-demo.ts +1015 -0
- package/src/examples/simple-layout-example.ts +591 -0
- package/src/examples/slider-demo.ts +617 -0
- package/src/examples/split-mode-demo.ts +453 -0
- package/src/examples/sprite-animation-demo.ts +443 -0
- package/src/examples/sprite-particle-generator-demo.ts +486 -0
- package/src/examples/static-sprite-demo.ts +193 -0
- package/src/examples/sticky-scroll-example.ts +308 -0
- package/src/examples/styled-text-demo.ts +282 -0
- package/src/examples/tab-select-demo.ts +219 -0
- package/src/examples/terminal-title.ts +29 -0
- package/src/examples/terminal.ts +305 -0
- package/src/examples/text-node-demo.ts +416 -0
- package/src/examples/text-selection-demo.ts +377 -0
- package/src/examples/text-table-demo.ts +503 -0
- package/src/examples/text-truncation-demo.ts +481 -0
- package/src/examples/text-wrap.ts +757 -0
- package/src/examples/texture-loading-demo.ts +259 -0
- package/src/examples/timeline-example.ts +670 -0
- package/src/examples/transparency-demo.ts +400 -0
- package/src/examples/vnode-composition-demo.ts +404 -0
- package/src/examples/wide-grapheme-overlay-demo.ts +280 -0
- package/src/index.ts +24 -0
- package/src/lib/KeyHandler.integration.test.ts +292 -0
- package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
- package/src/lib/KeyHandler.test.ts +662 -0
- package/src/lib/KeyHandler.ts +222 -0
- package/src/lib/RGBA.test.ts +984 -0
- package/src/lib/RGBA.ts +204 -0
- package/src/lib/ascii.font.ts +330 -0
- package/src/lib/border.test.ts +83 -0
- package/src/lib/border.ts +170 -0
- package/src/lib/bunfs.test.ts +27 -0
- package/src/lib/bunfs.ts +18 -0
- package/src/lib/clipboard.test.ts +41 -0
- package/src/lib/clipboard.ts +47 -0
- package/src/lib/clock.ts +35 -0
- package/src/lib/data-paths.test.ts +133 -0
- package/src/lib/data-paths.ts +109 -0
- package/src/lib/debounce.ts +106 -0
- package/src/lib/detect-links.test.ts +98 -0
- package/src/lib/detect-links.ts +56 -0
- package/src/lib/env.test.ts +228 -0
- package/src/lib/env.ts +209 -0
- package/src/lib/extmarks-history.ts +51 -0
- package/src/lib/extmarks-multiwidth.test.ts +322 -0
- package/src/lib/extmarks.test.ts +3457 -0
- package/src/lib/extmarks.ts +843 -0
- package/src/lib/fonts/block.json +405 -0
- package/src/lib/fonts/grid.json +265 -0
- package/src/lib/fonts/huge.json +741 -0
- package/src/lib/fonts/pallet.json +314 -0
- package/src/lib/fonts/shade.json +591 -0
- package/src/lib/fonts/slick.json +321 -0
- package/src/lib/fonts/tiny.json +69 -0
- package/src/lib/hast-styled-text.ts +59 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/keymapping.test.ts +317 -0
- package/src/lib/keymapping.ts +115 -0
- package/src/lib/objects-in-viewport.test.ts +787 -0
- package/src/lib/objects-in-viewport.ts +153 -0
- package/src/lib/output.capture.ts +58 -0
- package/src/lib/parse.keypress-kitty.protocol.test.ts +340 -0
- package/src/lib/parse.keypress-kitty.test.ts +663 -0
- package/src/lib/parse.keypress-kitty.ts +439 -0
- package/src/lib/parse.keypress.test.ts +1849 -0
- package/src/lib/parse.keypress.ts +397 -0
- package/src/lib/parse.mouse.test.ts +552 -0
- package/src/lib/parse.mouse.ts +232 -0
- package/src/lib/paste.ts +16 -0
- package/src/lib/queue.ts +65 -0
- package/src/lib/renderable.validations.test.ts +87 -0
- package/src/lib/renderable.validations.ts +83 -0
- package/src/lib/scroll-acceleration.ts +98 -0
- package/src/lib/selection.ts +240 -0
- package/src/lib/singleton.ts +28 -0
- package/src/lib/stdin-parser.test.ts +2290 -0
- package/src/lib/stdin-parser.ts +1810 -0
- package/src/lib/styled-text.ts +178 -0
- package/src/lib/terminal-capability-detection.test.ts +202 -0
- package/src/lib/terminal-capability-detection.ts +79 -0
- package/src/lib/terminal-palette.test.ts +878 -0
- package/src/lib/terminal-palette.ts +383 -0
- package/src/lib/tree-sitter/assets/README.md +118 -0
- package/src/lib/tree-sitter/assets/update.ts +334 -0
- package/src/lib/tree-sitter/assets.d.ts +9 -0
- package/src/lib/tree-sitter/cache.test.ts +273 -0
- package/src/lib/tree-sitter/client.test.ts +1165 -0
- package/src/lib/tree-sitter/client.ts +607 -0
- package/src/lib/tree-sitter/default-parsers.ts +86 -0
- package/src/lib/tree-sitter/download-utils.ts +148 -0
- package/src/lib/tree-sitter/index.ts +28 -0
- package/src/lib/tree-sitter/parser.worker.ts +1042 -0
- package/src/lib/tree-sitter/parsers-config.ts +81 -0
- package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
- package/src/lib/tree-sitter/resolve-ft.ts +189 -0
- package/src/lib/tree-sitter/types.ts +82 -0
- package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
- package/src/lib/tree-sitter-styled-text.ts +306 -0
- package/src/lib/validate-dir-name.ts +55 -0
- package/src/lib/yoga.options.test.ts +628 -0
- package/src/lib/yoga.options.ts +346 -0
- package/src/plugins/core-slot.ts +579 -0
- package/src/plugins/registry.ts +402 -0
- package/src/plugins/types.ts +46 -0
- package/src/post/effects.ts +930 -0
- package/src/post/filters.ts +489 -0
- package/src/post/matrices.ts +288 -0
- package/src/renderables/ASCIIFont.ts +219 -0
- package/src/renderables/Box.test.ts +205 -0
- package/src/renderables/Box.ts +326 -0
- package/src/renderables/Code.test.ts +2062 -0
- package/src/renderables/Code.ts +357 -0
- package/src/renderables/Diff.regression.test.ts +226 -0
- package/src/renderables/Diff.test.ts +3101 -0
- package/src/renderables/Diff.ts +1211 -0
- package/src/renderables/EditBufferRenderable.test.ts +288 -0
- package/src/renderables/EditBufferRenderable.ts +1166 -0
- package/src/renderables/FrameBuffer.ts +47 -0
- package/src/renderables/Input.test.ts +1228 -0
- package/src/renderables/Input.ts +247 -0
- package/src/renderables/LineNumberRenderable.ts +724 -0
- package/src/renderables/Markdown.ts +1393 -0
- package/src/renderables/ScrollBar.ts +422 -0
- package/src/renderables/ScrollBox.ts +883 -0
- package/src/renderables/Select.test.ts +1033 -0
- package/src/renderables/Select.ts +524 -0
- package/src/renderables/Slider.test.ts +456 -0
- package/src/renderables/Slider.ts +342 -0
- package/src/renderables/TabSelect.test.ts +197 -0
- package/src/renderables/TabSelect.ts +455 -0
- package/src/renderables/Text.selection-buffer.test.ts +123 -0
- package/src/renderables/Text.test.ts +2660 -0
- package/src/renderables/Text.ts +147 -0
- package/src/renderables/TextBufferRenderable.ts +518 -0
- package/src/renderables/TextNode.test.ts +1058 -0
- package/src/renderables/TextNode.ts +325 -0
- package/src/renderables/TextTable.test.ts +1421 -0
- package/src/renderables/TextTable.ts +1344 -0
- package/src/renderables/Textarea.ts +430 -0
- package/src/renderables/TimeToFirstDraw.ts +89 -0
- package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
- package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
- package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
- package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
- package/src/renderables/__tests__/LineNumberRenderable.test.ts +1865 -0
- package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
- package/src/renderables/__tests__/Markdown.code-colors.test.ts +242 -0
- package/src/renderables/__tests__/Markdown.test.ts +2518 -0
- package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
- package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
- package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
- package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -0
- package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
- package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
- package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
- package/src/renderables/__tests__/Textarea.keybinding.test.ts +3149 -0
- package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
- package/src/renderables/__tests__/Textarea.rendering.test.ts +1866 -0
- package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
- package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -0
- package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
- package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
- package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
- package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
- package/src/renderables/__tests__/markdown-parser.test.ts +217 -0
- package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
- package/src/renderables/composition/README.md +8 -0
- package/src/renderables/composition/VRenderable.ts +32 -0
- package/src/renderables/composition/constructs.ts +127 -0
- package/src/renderables/composition/vnode.ts +289 -0
- package/src/renderables/index.ts +23 -0
- package/src/renderables/markdown-parser.ts +66 -0
- package/src/renderer.ts +2681 -0
- package/src/runtime-plugin-support.ts +39 -0
- package/src/runtime-plugin.ts +615 -0
- package/src/syntax-style.test.ts +841 -0
- package/src/syntax-style.ts +257 -0
- package/src/testing/README.md +210 -0
- package/src/testing/capture-spans.test.ts +194 -0
- package/src/testing/integration.test.ts +276 -0
- package/src/testing/manual-clock.ts +117 -0
- package/src/testing/mock-keys.test.ts +1378 -0
- package/src/testing/mock-keys.ts +457 -0
- package/src/testing/mock-mouse.test.ts +218 -0
- package/src/testing/mock-mouse.ts +247 -0
- package/src/testing/mock-tree-sitter-client.ts +73 -0
- package/src/testing/spy.ts +13 -0
- package/src/testing/test-recorder.test.ts +415 -0
- package/src/testing/test-recorder.ts +145 -0
- package/src/testing/test-renderer.ts +132 -0
- package/src/testing.ts +7 -0
- package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
- package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
- package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
- package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
- package/src/tests/allocator-stats.test.ts +38 -0
- package/src/tests/destroy-during-render.test.ts +200 -0
- package/src/tests/destroy-on-exit.fixture.ts +36 -0
- package/src/tests/destroy-on-exit.test.ts +41 -0
- package/src/tests/hover-cursor.test.ts +98 -0
- package/src/tests/native-span-feed-async.test.ts +173 -0
- package/src/tests/native-span-feed-close.test.ts +120 -0
- package/src/tests/native-span-feed-coverage.test.ts +227 -0
- package/src/tests/native-span-feed-edge-cases.test.ts +352 -0
- package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
- package/src/tests/opacity.test.ts +123 -0
- package/src/tests/renderable.snapshot.test.ts +524 -0
- package/src/tests/renderable.test.ts +1281 -0
- package/src/tests/renderer.clock.test.ts +158 -0
- package/src/tests/renderer.console-startup.test.ts +185 -0
- package/src/tests/renderer.control.test.ts +425 -0
- package/src/tests/renderer.core-slot-binding.test.ts +952 -0
- package/src/tests/renderer.cursor.test.ts +26 -0
- package/src/tests/renderer.destroy-during-render.test.ts +147 -0
- package/src/tests/renderer.focus-restore.test.ts +257 -0
- package/src/tests/renderer.focus.test.ts +294 -0
- package/src/tests/renderer.idle.test.ts +219 -0
- package/src/tests/renderer.input.test.ts +2237 -0
- package/src/tests/renderer.kitty-flags.test.ts +195 -0
- package/src/tests/renderer.mouse.test.ts +1274 -0
- package/src/tests/renderer.palette.test.ts +629 -0
- package/src/tests/renderer.selection.test.ts +49 -0
- package/src/tests/renderer.slot-registry.test.ts +684 -0
- package/src/tests/renderer.useMouse.test.ts +47 -0
- package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
- package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
- package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
- package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
- package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
- package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
- package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
- package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
- package/src/tests/runtime-plugin-support.fixture.ts +11 -0
- package/src/tests/runtime-plugin-support.test.ts +19 -0
- package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
- package/src/tests/runtime-plugin.fixture.ts +40 -0
- package/src/tests/runtime-plugin.test.ts +354 -0
- package/src/tests/scrollbox-culling-bug.test.ts +114 -0
- package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
- package/src/tests/scrollbox-hitgrid.test.ts +909 -0
- package/src/tests/scrollbox.test.ts +1530 -0
- package/src/tests/wrap-resize-perf.test.ts +276 -0
- package/src/tests/yoga-setters.test.ts +921 -0
- package/src/text-buffer-view.test.ts +705 -0
- package/src/text-buffer-view.ts +189 -0
- package/src/text-buffer.test.ts +347 -0
- package/src/text-buffer.ts +250 -0
- package/src/types.ts +161 -0
- package/src/utils.ts +88 -0
- package/src/zig/ansi.zig +268 -0
- package/src/zig/bench/README.md +50 -0
- package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
- package/src/zig/bench/edit-buffer_bench.zig +476 -0
- package/src/zig/bench/native-span-feed_bench.zig +100 -0
- package/src/zig/bench/rope-markers_bench.zig +713 -0
- package/src/zig/bench/rope_bench.zig +514 -0
- package/src/zig/bench/styled-text_bench.zig +470 -0
- package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
- package/src/zig/bench/text-buffer-view_bench.zig +459 -0
- package/src/zig/bench/text-chunk-graphemes_bench.zig +273 -0
- package/src/zig/bench/utf8_bench.zig +799 -0
- package/src/zig/bench-utils.zig +431 -0
- package/src/zig/bench.zig +217 -0
- package/src/zig/buffer-methods.zig +211 -0
- package/src/zig/buffer.zig +2281 -0
- package/src/zig/build.zig +289 -0
- package/src/zig/build.zig.zon +16 -0
- package/src/zig/edit-buffer.zig +825 -0
- package/src/zig/editor-view.zig +802 -0
- package/src/zig/event-bus.zig +13 -0
- package/src/zig/event-emitter.zig +65 -0
- package/src/zig/file-logger.zig +92 -0
- package/src/zig/grapheme.zig +599 -0
- package/src/zig/lib.zig +1854 -0
- package/src/zig/link.zig +333 -0
- package/src/zig/logger.zig +43 -0
- package/src/zig/mem-registry.zig +125 -0
- package/src/zig/native-span-feed-bench-lib.zig +7 -0
- package/src/zig/native-span-feed.zig +708 -0
- package/src/zig/renderer.zig +1393 -0
- package/src/zig/rope.zig +1220 -0
- package/src/zig/syntax-style.zig +161 -0
- package/src/zig/terminal.zig +987 -0
- package/src/zig/test.zig +72 -0
- package/src/zig/tests/README.md +18 -0
- package/src/zig/tests/buffer-methods_test.zig +1109 -0
- package/src/zig/tests/buffer_test.zig +2557 -0
- package/src/zig/tests/edit-buffer-history_test.zig +271 -0
- package/src/zig/tests/edit-buffer_test.zig +1689 -0
- package/src/zig/tests/editor-view_test.zig +3299 -0
- package/src/zig/tests/event-emitter_test.zig +249 -0
- package/src/zig/tests/grapheme_test.zig +1304 -0
- package/src/zig/tests/link_test.zig +190 -0
- package/src/zig/tests/mem-registry_test.zig +473 -0
- package/src/zig/tests/memory_leak_regression_test.zig +159 -0
- package/src/zig/tests/native-span-feed_test.zig +1264 -0
- package/src/zig/tests/renderer_test.zig +1017 -0
- package/src/zig/tests/rope-nested_test.zig +712 -0
- package/src/zig/tests/rope_fuzz_test.zig +238 -0
- package/src/zig/tests/rope_test.zig +2362 -0
- package/src/zig/tests/segment-merge.test.zig +148 -0
- package/src/zig/tests/syntax-style_test.zig +557 -0
- package/src/zig/tests/terminal_test.zig +754 -0
- package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
- package/src/zig/tests/text-buffer-highlights_test.zig +666 -0
- package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
- package/src/zig/tests/text-buffer-segment_test.zig +320 -0
- package/src/zig/tests/text-buffer-selection_test.zig +1035 -0
- package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
- package/src/zig/tests/text-buffer-view_test.zig +3649 -0
- package/src/zig/tests/text-buffer_test.zig +2191 -0
- package/src/zig/tests/unicode-width-map.zon +3909 -0
- package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
- package/src/zig/tests/utf8_test.zig +4057 -0
- package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
- package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
- package/src/zig/tests/word-wrap-editing_test.zig +498 -0
- package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
- package/src/zig/text-buffer-iterators.zig +499 -0
- package/src/zig/text-buffer-segment.zig +404 -0
- package/src/zig/text-buffer-view.zig +1371 -0
- package/src/zig/text-buffer.zig +1180 -0
- package/src/zig/utf8.zig +1948 -0
- package/src/zig/utils.zig +9 -0
- package/src/zig-structs.ts +261 -0
- package/src/zig.ts +3884 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +27 -0
- package/3d/SpriteResourceManager.d.ts +0 -74
- package/3d/SpriteUtils.d.ts +0 -13
- package/3d/TextureUtils.d.ts +0 -24
- package/3d/ThreeRenderable.d.ts +0 -40
- package/3d/WGPURenderer.d.ts +0 -61
- package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
- package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
- package/3d/animation/SpriteAnimator.d.ts +0 -124
- package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
- package/3d/canvas.d.ts +0 -44
- package/3d/index.d.ts +0 -12
- package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
- package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
- package/3d/physics/physics-interface.d.ts +0 -27
- package/3d.d.ts +0 -2
- package/3d.js +0 -34041
- package/3d.js.map +0 -155
- package/LICENSE +0 -21
- package/NativeSpanFeed.d.ts +0 -41
- package/Renderable.d.ts +0 -334
- package/animation/Timeline.d.ts +0 -126
- package/ansi.d.ts +0 -13
- package/buffer.d.ts +0 -111
- package/console.d.ts +0 -144
- package/edit-buffer.d.ts +0 -98
- package/editor-view.d.ts +0 -73
- package/index-9vwc3fg6.js +0 -12260
- package/index-9vwc3fg6.js.map +0 -42
- package/index-dcj62y8t.js +0 -20614
- package/index-dcj62y8t.js.map +0 -67
- package/index-f7n39gpy.js +0 -411
- package/index-f7n39gpy.js.map +0 -10
- package/index.d.ts +0 -23
- package/index.js +0 -478
- package/index.js.map +0 -9
- package/lib/KeyHandler.d.ts +0 -61
- package/lib/RGBA.d.ts +0 -25
- package/lib/ascii.font.d.ts +0 -508
- package/lib/border.d.ts +0 -51
- package/lib/bunfs.d.ts +0 -7
- package/lib/clipboard.d.ts +0 -17
- package/lib/clock.d.ts +0 -15
- package/lib/data-paths.d.ts +0 -26
- package/lib/debounce.d.ts +0 -42
- package/lib/detect-links.d.ts +0 -6
- package/lib/env.d.ts +0 -42
- package/lib/extmarks-history.d.ts +0 -17
- package/lib/extmarks.d.ts +0 -89
- package/lib/hast-styled-text.d.ts +0 -17
- package/lib/index.d.ts +0 -21
- package/lib/keymapping.d.ts +0 -25
- package/lib/objects-in-viewport.d.ts +0 -24
- package/lib/output.capture.d.ts +0 -24
- package/lib/parse.keypress-kitty.d.ts +0 -2
- package/lib/parse.keypress.d.ts +0 -26
- package/lib/parse.mouse.d.ts +0 -30
- package/lib/paste.d.ts +0 -7
- package/lib/queue.d.ts +0 -15
- package/lib/renderable.validations.d.ts +0 -12
- package/lib/scroll-acceleration.d.ts +0 -43
- package/lib/selection.d.ts +0 -63
- package/lib/singleton.d.ts +0 -7
- package/lib/stdin-parser.d.ts +0 -87
- package/lib/styled-text.d.ts +0 -63
- package/lib/terminal-capability-detection.d.ts +0 -30
- package/lib/terminal-palette.d.ts +0 -50
- package/lib/tree-sitter/assets/update.d.ts +0 -11
- package/lib/tree-sitter/client.d.ts +0 -47
- package/lib/tree-sitter/default-parsers.d.ts +0 -2
- package/lib/tree-sitter/download-utils.d.ts +0 -21
- package/lib/tree-sitter/index.d.ts +0 -8
- package/lib/tree-sitter/parser.worker.d.ts +0 -1
- package/lib/tree-sitter/parsers-config.d.ts +0 -53
- package/lib/tree-sitter/resolve-ft.d.ts +0 -5
- package/lib/tree-sitter/types.d.ts +0 -82
- package/lib/tree-sitter-styled-text.d.ts +0 -14
- package/lib/validate-dir-name.d.ts +0 -1
- package/lib/yoga.options.d.ts +0 -32
- package/parser.worker.js +0 -899
- package/parser.worker.js.map +0 -12
- package/plugins/core-slot.d.ts +0 -72
- package/plugins/registry.d.ts +0 -42
- package/plugins/types.d.ts +0 -34
- package/post/effects.d.ts +0 -147
- package/post/filters.d.ts +0 -65
- package/post/matrices.d.ts +0 -20
- package/renderables/ASCIIFont.d.ts +0 -52
- package/renderables/Box.d.ts +0 -81
- package/renderables/Code.d.ts +0 -78
- package/renderables/Diff.d.ts +0 -142
- package/renderables/EditBufferRenderable.d.ts +0 -237
- package/renderables/FrameBuffer.d.ts +0 -16
- package/renderables/Input.d.ts +0 -67
- package/renderables/LineNumberRenderable.d.ts +0 -78
- package/renderables/Markdown.d.ts +0 -185
- package/renderables/ScrollBar.d.ts +0 -77
- package/renderables/ScrollBox.d.ts +0 -124
- package/renderables/Select.d.ts +0 -115
- package/renderables/Slider.d.ts +0 -47
- package/renderables/TabSelect.d.ts +0 -96
- package/renderables/Text.d.ts +0 -36
- package/renderables/TextBufferRenderable.d.ts +0 -105
- package/renderables/TextNode.d.ts +0 -91
- package/renderables/TextTable.d.ts +0 -140
- package/renderables/Textarea.d.ts +0 -63
- package/renderables/TimeToFirstDraw.d.ts +0 -24
- package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
- package/renderables/composition/VRenderable.d.ts +0 -16
- package/renderables/composition/constructs.d.ts +0 -35
- package/renderables/composition/vnode.d.ts +0 -46
- package/renderables/index.d.ts +0 -23
- package/renderables/markdown-parser.d.ts +0 -10
- package/renderer.d.ts +0 -419
- package/runtime-plugin-support.d.ts +0 -3
- package/runtime-plugin-support.js +0 -29
- package/runtime-plugin-support.js.map +0 -10
- package/runtime-plugin.d.ts +0 -16
- package/runtime-plugin.js +0 -16
- package/runtime-plugin.js.map +0 -9
- package/syntax-style.d.ts +0 -54
- package/testing/manual-clock.d.ts +0 -17
- package/testing/mock-keys.d.ts +0 -81
- package/testing/mock-mouse.d.ts +0 -38
- package/testing/mock-tree-sitter-client.d.ts +0 -23
- package/testing/spy.d.ts +0 -7
- package/testing/test-recorder.d.ts +0 -61
- package/testing/test-renderer.d.ts +0 -23
- package/testing.d.ts +0 -6
- package/testing.js +0 -697
- package/testing.js.map +0 -15
- package/text-buffer-view.d.ts +0 -42
- package/text-buffer.d.ts +0 -67
- package/types.d.ts +0 -139
- package/utils.d.ts +0 -14
- package/zig-structs.d.ts +0 -155
- package/zig.d.ts +0 -353
- /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/zig/tree-sitter-zig.wasm +0 -0
|
@@ -0,0 +1,2518 @@
|
|
|
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
|
+
})
|