@linxiraos/pi-tui 1.1.26 → 1.2.0
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/CHANGELOG.md +51 -491
- package/README.md +13 -7
- package/dist/types/active-terminal.d.ts +30 -0
- package/dist/types/app-keybindings.d.ts +7 -2
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +5 -4
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +11 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +17 -4
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/status-notice.d.ts +15 -3
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +23 -5
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +26 -44
- package/dist/types/components/input.d.ts +2 -1
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/loader.d.ts +5 -6
- package/dist/types/components/markdown.d.ts +47 -5
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +9 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/apply.d.ts +4 -3
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +8 -6
- package/dist/types/native/node.d.ts +36 -5
- package/dist/types/native/overlay.d.ts +8 -3
- package/dist/types/native/settle.d.ts +4 -3
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +3 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/btw-history-panel.d.ts +13 -1
- package/dist/types/overlays/copy-selector.d.ts +1 -0
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/hub-frame.d.ts +4 -1
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +37 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/report-panel.d.ts +68 -0
- package/dist/types/overlays/rewind-selector.d.ts +1 -0
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/thinking-selector.d.ts +2 -2
- package/dist/types/overlays/usage-dashboard.d.ts +12 -8
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer-cache.d.ts +0 -16
- package/dist/types/prompt/composer.d.ts +18 -3
- package/dist/types/prompt/custom-editor.d.ts +25 -13
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +7 -2
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/setup/scenes/types.d.ts +1 -0
- package/dist/types/space-hold.d.ts +40 -25
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/context-usage.d.ts +7 -5
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +41 -36
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +23 -16
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/thinking.d.ts +1 -1
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/bash.d.ts +7 -2
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +2 -3
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +36 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/dist/types/windows-input-mode.d.ts +33 -1
- package/package.json +13 -9
- package/src/active-terminal.d.ts +30 -0
- package/src/active-terminal.ts +80 -0
- package/src/app-keybindings.d.ts +7 -2
- package/src/app-keybindings.ts +7 -2
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +96 -23
- package/src/apps/git/git-tui.ts +23 -16
- package/src/apps/git/help.ts +29 -20
- package/src/apps/git/sidebar.ts +8 -3
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/bracketed-paste.ts +10 -3
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +11 -1
- package/src/chat/assistant-message.ts +245 -54
- package/src/chat/chat-transcript-builder.d.ts +3 -0
- package/src/chat/chat-transcript-builder.ts +9 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +86 -7
- package/src/chat/user-message.d.ts +17 -4
- package/src/chat/user-message.ts +71 -8
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/status-notice.ts +25 -7
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +162 -19
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +23 -5
- package/src/components/editor.ts +316 -47
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +26 -44
- package/src/components/image.ts +115 -152
- package/src/components/input.d.ts +2 -1
- package/src/components/input.ts +17 -7
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/loader.d.ts +5 -6
- package/src/components/loader.ts +10 -17
- package/src/components/markdown.d.ts +47 -5
- package/src/components/markdown.ts +786 -303
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +3 -2
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/hotkeys-markdown.ts +9 -2
- package/src/key-hint-format.d.ts +9 -1
- package/src/key-hint-format.ts +26 -11
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +11 -5
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/apply.d.ts +4 -3
- package/src/native/apply.ts +4 -3
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +199 -22
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +8 -6
- package/src/native/encode.ts +76 -34
- package/src/native/node.d.ts +36 -5
- package/src/native/node.ts +31 -5
- package/src/native/overlay.d.ts +8 -3
- package/src/native/overlay.ts +17 -4
- package/src/native/reconcile.ts +16 -4
- package/src/native/settle.d.ts +4 -3
- package/src/native/settle.ts +4 -3
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +48 -29
- package/src/overlays/agent-transcript-viewer.ts +93 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +10 -17
- package/src/overlays/btw-history-panel.ts +136 -39
- package/src/overlays/copy-selector.d.ts +1 -0
- package/src/overlays/copy-selector.ts +32 -8
- package/src/overlays/extensions/extension-dashboard.ts +4 -4
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/hub-frame.ts +19 -1
- package/src/overlays/jobs-panel.ts +33 -52
- package/src/overlays/model-browser.ts +367 -122
- package/src/overlays/model-hub.ts +435 -59
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/pause-screen.ts +2 -2
- package/src/overlays/plan-review-overlay.ts +208 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +191 -0
- package/src/overlays/rewind-selector.ts +88 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +5 -4
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/thinking-selector.ts +11 -10
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +117 -58
- package/src/overlays/usage-display.ts +10 -4
- package/src/overlays/usage-row.ts +22 -8
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer-cache.ts +7 -86
- package/src/prompt/composer.ts +37 -26
- package/src/prompt/custom-editor.ts +282 -105
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +32 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/queued-messages.ts +6 -4
- package/src/prompt/tips.txt +13 -9
- package/src/prompt/welcome.ts +64 -46
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +16 -1
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/types.ts +1 -0
- package/src/space-hold.d.ts +40 -25
- package/src/space-hold.ts +156 -52
- package/src/status-line/component.ts +292 -78
- package/src/status-line/context-usage.ts +21 -46
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +41 -36
- package/src/terminal-capabilities.ts +137 -213
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +23 -16
- package/src/terminal.ts +203 -99
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +11 -4
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/thinking.d.ts +1 -1
- package/src/thinking.ts +1 -1
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.d.ts +7 -2
- package/src/tools/bash.ts +116 -47
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +154 -50
- package/src/tools/task.d.ts +2 -3
- package/src/tools/task.ts +115 -62
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +36 -0
- package/src/tui.ts +666 -217
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.d.ts +33 -1
- package/src/windows-input-mode.ts +212 -6
|
@@ -6,8 +6,18 @@ import {
|
|
|
6
6
|
type TokenizerAndRendererExtension,
|
|
7
7
|
type TokenizerThis,
|
|
8
8
|
type Tokens,
|
|
9
|
+
type TokensList,
|
|
9
10
|
} from "@linxiraos/pi-utils/marked";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type MathBlockOpener,
|
|
13
|
+
mathBlockCloserIndex,
|
|
14
|
+
mathBlockInContext,
|
|
15
|
+
mathBlockMayCloseAt,
|
|
16
|
+
mathBlockOpenerAt,
|
|
17
|
+
mathSpanInContext,
|
|
18
|
+
mathStartIndex,
|
|
19
|
+
} from "@linxiraos/pi-utils/math-delimiters";
|
|
20
|
+
import { listMayContinueAt } from "@linxiraos/pi-utils/marked-list";
|
|
11
21
|
import { latexToBlock } from "../latex-block";
|
|
12
22
|
import { isBareMathEnvironment, latexToUnicode } from "../latex-to-unicode";
|
|
13
23
|
import { plainText } from "../native/spans";
|
|
@@ -15,6 +25,8 @@ import { md } from "../native/describe";
|
|
|
15
25
|
import type { DescribeContext, NativeNode } from "../native/node";
|
|
16
26
|
import type { SymbolTheme } from "../symbols";
|
|
17
27
|
import { TERMINAL } from "../terminal-capabilities";
|
|
28
|
+
import { getSymbolTheme, getThemeEpoch } from "../theme/theme";
|
|
29
|
+
import { canCacheMarkdownListItems } from "../theme/tui-adapters";
|
|
18
30
|
import type { Component } from "../tui";
|
|
19
31
|
import {
|
|
20
32
|
applyBackgroundToLine,
|
|
@@ -592,7 +604,6 @@ const markdownParser = new Marked();
|
|
|
592
604
|
// never math. Inline extensions run before marked's escape tokenizer, so
|
|
593
605
|
// `\(…\)` becomes math while a genuinely escaped `\$` is left to `escape` and
|
|
594
606
|
// renders as a literal dollar.
|
|
595
|
-
const CUSTOM_HR_START_REGEX = /(?:^|\n) {0,3}([-*_─━═=–—])[ \t]*(?:\1[ \t]*){2,}(?:\n+|$)/;
|
|
596
607
|
const CUSTOM_HR_TOKENIZER_REGEX = /^ {0,3}([-*_─━═=–—])[ \t]*(?:\1[ \t]*){2,}(?:\n+|$)/;
|
|
597
608
|
|
|
598
609
|
function getHrChar(char: string, hrChar: string): string {
|
|
@@ -618,15 +629,6 @@ function getHrChar(char: string, hrChar: string): string {
|
|
|
618
629
|
const customHrExtension: TokenizerAndRendererExtension = {
|
|
619
630
|
name: "customHr",
|
|
620
631
|
level: "block",
|
|
621
|
-
start(src) {
|
|
622
|
-
const match = CUSTOM_HR_START_REGEX.exec(src);
|
|
623
|
-
if (!match) return undefined;
|
|
624
|
-
let idx = match.index;
|
|
625
|
-
if (src[idx] === "\n") {
|
|
626
|
-
idx += 1;
|
|
627
|
-
}
|
|
628
|
-
return idx;
|
|
629
|
-
},
|
|
630
632
|
tokenizer(src) {
|
|
631
633
|
const match = CUSTOM_HR_TOKENIZER_REGEX.exec(src);
|
|
632
634
|
if (match) {
|
|
@@ -663,7 +665,7 @@ const mathBlockExtension: TokenizerAndRendererExtension = {
|
|
|
663
665
|
// No `start` hint: marked only probes block extensions at a block boundary
|
|
664
666
|
// here and never consults their hints.
|
|
665
667
|
tokenizer(src) {
|
|
666
|
-
const block =
|
|
668
|
+
const block = mathBlockInContext(this, src);
|
|
667
669
|
if (!block) return undefined;
|
|
668
670
|
return { type: "math", raw: block.raw, text: block.body, display: true };
|
|
669
671
|
},
|
|
@@ -680,38 +682,67 @@ const mathBlockExtension: TokenizerAndRendererExtension = {
|
|
|
680
682
|
// starts at offset 0" guards keep fenced/indented `\begin{cases}` code blocks
|
|
681
683
|
// for marked's own code rules.
|
|
682
684
|
const BARE_ENV_BEGIN = /(?:^|\n)[ \t]{0,3}\\begin\{([A-Za-z]+\*?)\}/;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
685
|
+
const BLANK_LINE_RE = /\n[ \t]*\n/;
|
|
686
|
+
|
|
687
|
+
/** Where a search for an `\end{…}` closer in a lexer source started, and what it found. */
|
|
688
|
+
interface EnvCloserSearch {
|
|
689
|
+
from: number;
|
|
690
|
+
at: number;
|
|
691
|
+
}
|
|
692
|
+
const envCloserSearches = new WeakMap<TokenizerThis, Map<string, EnvCloserSearch>>();
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* The first `endToken` at or after `from` in the source of `context`, or -1. Block
|
|
696
|
+
* starts ask at increasing offsets, so a search is remembered per source: an
|
|
697
|
+
* unclosed environment otherwise rescans the rest of the document at each one.
|
|
698
|
+
*/
|
|
699
|
+
function envCloserIndex(context: TokenizerThis, source: string, endToken: string, from: number): number {
|
|
700
|
+
let searches = envCloserSearches.get(context);
|
|
701
|
+
if (!searches) envCloserSearches.set(context, (searches = new Map()));
|
|
702
|
+
const last = searches.get(endToken);
|
|
703
|
+
if (last !== undefined && last.from <= from && (last.at === -1 || from <= last.at)) return last.at;
|
|
704
|
+
const at = source.indexOf(endToken, from);
|
|
705
|
+
searches.set(endToken, { from, at });
|
|
706
|
+
return at;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Length of the bare math-environment block that starts at offset 0 of `src`, or 0. */
|
|
710
|
+
function bareMathEnvBlockLength(context: TokenizerThis, src: string): number {
|
|
711
|
+
// A block at offset 0 has its `\begin` on the first line, or on the second
|
|
712
|
+
// after an `lhs =` line, so the opener scan stops there.
|
|
713
|
+
const firstBreak = src.indexOf("\n");
|
|
714
|
+
const secondBreak = firstBreak === -1 ? -1 : src.indexOf("\n", firstBreak + 1);
|
|
715
|
+
const bm = BARE_ENV_BEGIN.exec(secondBreak === -1 ? src : src.slice(0, secondBreak));
|
|
716
|
+
if (!bm || !isBareMathEnvironment(bm[1])) return 0;
|
|
717
|
+
// Skip a matched leading `\n`, at offset 0 too: a blank line before the block
|
|
718
|
+
// is a space token of its own, not part of the block.
|
|
719
|
+
const beginLineStart = src[bm.index] === "\n" ? bm.index + 1 : bm.index;
|
|
720
|
+
// Pull in one immediately-preceding `lhs =`/open-delimiter line (e.g. `f(x) =`).
|
|
721
|
+
if (beginLineStart > 0 && !/[=([{]\s*$/.test(src.slice(0, beginLineStart - 1))) return 0;
|
|
687
722
|
const endToken = `\\end{${bm[1]}}`;
|
|
688
|
-
const
|
|
689
|
-
|
|
723
|
+
const source = context.source ?? src;
|
|
724
|
+
const base = (context.end ?? source.length) - src.length;
|
|
725
|
+
const closer = envCloserIndex(context, source, endToken, base + bm.index);
|
|
726
|
+
if (closer === -1) return 0;
|
|
727
|
+
const endAt = closer - base;
|
|
690
728
|
// The `\end` must close before any blank line (i.e. within the same block).
|
|
691
|
-
|
|
729
|
+
// Scan only up to the closer: the rest of the document is irrelevant here, and
|
|
730
|
+
// scanning it per block is quadratic across consecutive environments.
|
|
731
|
+
if (BLANK_LINE_RE.test(src.slice(beginLineStart, endAt))) return 0;
|
|
692
732
|
let blockEnd = endAt + endToken.length;
|
|
693
733
|
while (src[blockEnd] === " " || src[blockEnd] === "\t") blockEnd++;
|
|
694
734
|
if (src[blockEnd] === "\n") blockEnd++;
|
|
695
|
-
|
|
696
|
-
let start = beginLineStart;
|
|
697
|
-
if (start > 0 && src[start - 1] === "\n") {
|
|
698
|
-
const prevStart = src.lastIndexOf("\n", start - 2) + 1;
|
|
699
|
-
const prevLine = src.slice(prevStart, start - 1);
|
|
700
|
-
if (/[=([{]\s*$/.test(prevLine)) start = prevStart;
|
|
701
|
-
}
|
|
702
|
-
return [start, blockEnd];
|
|
735
|
+
return blockEnd;
|
|
703
736
|
}
|
|
737
|
+
|
|
704
738
|
const mathEnvBlockExtension: TokenizerAndRendererExtension = {
|
|
705
739
|
name: "mathEnvBlock",
|
|
706
740
|
level: "block",
|
|
707
|
-
start
|
|
708
|
-
const r = bareMathEnvBlock(src);
|
|
709
|
-
return r ? r[0] : undefined;
|
|
710
|
-
},
|
|
741
|
+
// No `start` hint: marked only probes block extensions at a block boundary.
|
|
711
742
|
tokenizer(src) {
|
|
712
|
-
const
|
|
713
|
-
if (
|
|
714
|
-
const raw = src.slice(0,
|
|
743
|
+
const length = bareMathEnvBlockLength(this, src);
|
|
744
|
+
if (length === 0) return undefined;
|
|
745
|
+
const raw = src.slice(0, length);
|
|
715
746
|
const text = raw.replace(/\n[ \t]*$/, "");
|
|
716
747
|
if (text.trim().length === 0) return undefined;
|
|
717
748
|
return { type: "math", raw, text, display: true };
|
|
@@ -761,77 +792,13 @@ markdownParser.use({
|
|
|
761
792
|
],
|
|
762
793
|
});
|
|
763
794
|
|
|
764
|
-
// Setext-underline pre-gate for marked's `lheading` rule. The rule's lazy body
|
|
765
|
-
// `((?:.|\n(?!<block-start>))+?)` re-runs its block-start lookahead while
|
|
766
|
-
// expanding character by character, so even a FAILING attempt at offset 0
|
|
767
|
-
// costs O(len × lookahead) — ~26µs per 200-char list-item body, and marked's
|
|
768
|
-
// list tokenizer block-tokenizes every item's content (47.8% of a streaming
|
|
769
|
-
// bench profile). A match REQUIRES the setext underline `\n {0,3}(=+|-+)`
|
|
770
|
-
// somewhere in src, so this O(n) charCode scan never rejects a src the
|
|
771
|
-
// built-in rule would match; single-line srcs (every tight list item) reject
|
|
772
|
-
// on the first indexOf.
|
|
773
|
-
function lheadingPossible(src: string): boolean {
|
|
774
|
-
let i = src.indexOf("\n");
|
|
775
|
-
while (i !== -1) {
|
|
776
|
-
let j = i + 1;
|
|
777
|
-
const limit = j + 3; // underline allows up to 3 leading spaces
|
|
778
|
-
while (j < limit && src.charCodeAt(j) === 0x20 /* space */) j++;
|
|
779
|
-
const c = src.charCodeAt(j); // NaN past the end fails both comparisons
|
|
780
|
-
if (c === 0x3d /* = */ || c === 0x2d /* - */) return true;
|
|
781
|
-
i = src.indexOf("\n", j);
|
|
782
|
-
}
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
795
|
markdownParser.use({
|
|
787
796
|
tokenizer: {
|
|
788
|
-
// `false` → marked falls back to the built-in tokenizer;
|
|
789
|
-
// `undefined` → no token here, built-in never runs.
|
|
790
|
-
lheading(src: string): Tokens.Heading | undefined | false {
|
|
791
|
-
return lheadingPossible(src) ? false : undefined;
|
|
792
|
-
},
|
|
793
797
|
// Strikethrough is `strikethroughExtension`'s: marked's own looser rule never runs.
|
|
794
798
|
del: () => undefined,
|
|
795
799
|
},
|
|
796
800
|
});
|
|
797
801
|
|
|
798
|
-
// ---------------------------------------------------------------------------
|
|
799
|
-
// Sticky clones of marked's pathological block rules
|
|
800
|
-
// ---------------------------------------------------------------------------
|
|
801
|
-
// Bun's (JSC) regex engine skips the start-anchor fast-fail for several of
|
|
802
|
-
// marked's `^`-anchored block rules — `hr`, `lheading`, `table` and `html` are
|
|
803
|
-
// anchored alternations of quantified branches, and a failing `exec`/`test`
|
|
804
|
-
// rescans the entire remaining source instead of stopping after offset 0.
|
|
805
|
-
// marked's list tokenizer runs `hr.test` and `lheading` per list line against
|
|
806
|
-
// the remaining source, so lexing a long list is quadratic (66% of a streaming
|
|
807
|
-
// bench profile sat in these two regexes). A sticky (`y`) clone with
|
|
808
|
-
// `lastIndex` pinned to 0 attempts the match at offset 0 only.
|
|
809
|
-
//
|
|
810
|
-
// Equivalence: for a flagless rule whose source is `^`-anchored, a sticky
|
|
811
|
-
// clone at `lastIndex = 0` matches exactly when the original matches (same
|
|
812
|
-
// match object, same captures) — `^` already restricted matches to offset 0
|
|
813
|
-
// (no `m` flag), and stickiness only removes the futile later attempts. The
|
|
814
|
-
// flags/anchor guard below skips any rule a future marked version changes.
|
|
815
|
-
class AnchoredAtZero extends RegExp {
|
|
816
|
-
override exec(str: string): RegExpExecArray | null {
|
|
817
|
-
this.lastIndex = 0; // sticky matches set lastIndex; rules are shared
|
|
818
|
-
return super.exec(str);
|
|
819
|
-
}
|
|
820
|
-
override test(str: string): boolean {
|
|
821
|
-
this.lastIndex = 0;
|
|
822
|
-
return super.test(str);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
for (const table of [Lexer.rules.block.normal, Lexer.rules.block.gfm]) {
|
|
827
|
-
for (const name of ["hr", "lheading", "table", "html"] as const) {
|
|
828
|
-
const rule = table[name];
|
|
829
|
-
if (rule.flags === "" && rule.source.startsWith("^")) {
|
|
830
|
-
table[name] = new AnchoredAtZero(rule.source, "y");
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
802
|
// ---------------------------------------------------------------------------
|
|
836
803
|
// Module-level LRU render cache
|
|
837
804
|
// ---------------------------------------------------------------------------
|
|
@@ -897,12 +864,16 @@ function appendLines(dst: string[], src: readonly string[]): void {
|
|
|
897
864
|
// delta-edge trailingDelimiterSeamHazard check).
|
|
898
865
|
const FAST_DELTA_RE = /[\n\r\\[`<!*_~$#&@\x1b]/;
|
|
899
866
|
|
|
867
|
+
// Emphasis delimiters, which can pair across the fast path's seam.
|
|
868
|
+
const FAST_EMPHASIS_DELIMITERS = ["*", "_", "~"] as const;
|
|
869
|
+
|
|
900
870
|
// Disarm when the captured row's RAW tail ends in trailing whitespace (wrap
|
|
901
|
-
// trims it; appending a char moves the trim
|
|
902
|
-
// (it can become an escape once the delta
|
|
903
|
-
// clause covers that escape-completion
|
|
904
|
-
// run (a `#` + 3-8 hex is a swatch
|
|
905
|
-
|
|
871
|
+
// trims it, a no-break space included; appending a char moves the trim
|
|
872
|
+
// boundary), a trailing backslash (it can become an escape once the delta
|
|
873
|
+
// supplies the next char — the `\\` clause covers that escape-completion
|
|
874
|
+
// hazard), or a full/partial hex swatch run (a `#` + 3-8 hex is a swatch
|
|
875
|
+
// glyph; the byte range may shift).
|
|
876
|
+
const FAST_RUN_END_RE = /(?:[\s\\]|#[0-9a-fA-F]{3,8}|#+)$/i;
|
|
906
877
|
|
|
907
878
|
// A partial `#` + 1-2 hex digits can grow into a 3-8 digit swatch glyph
|
|
908
879
|
// across the seam (delta hex digits are inert).
|
|
@@ -957,16 +928,18 @@ const FAST_TABLE_DELIM_ROW_RE = /^\s*(?:\|[\s:]*-+\s*(?:\|[\s:]*-+\s*)*|[\s:]*-+
|
|
|
957
928
|
|
|
958
929
|
// A paragraph's LAST line can complete into a different block kind under an
|
|
959
930
|
// inert delta (ATX heading, blockquote, bullet marker, HR, ref-def) — disarm
|
|
960
|
-
// when the grown line starts one (ref-def grammar: REF_DEF_LINE_RE).
|
|
931
|
+
// when the grown line starts one (ref-def grammar: REF_DEF_LINE_RE). A line
|
|
932
|
+
// holding `\end{` can also close a bare math environment the paragraph opened
|
|
933
|
+
// lines above (`\end{al` + `ign}`), making those lines one display block.
|
|
961
934
|
const FAST_LINE_START_HAZARD_RE =
|
|
962
935
|
// `-` is placed LAST so it is a literal, not a range bound. The other
|
|
963
936
|
// chars are in ASCENDING code-point order (no reversed ranges that
|
|
964
937
|
// rely on engine leniency): * + = – — ─ ━ ═ then the literal `-`.
|
|
965
938
|
/^ {0,3}(?:#{1,6}(?:[ \t]|$)|>|\d{1,9}[.)](?:[ \t]|$)|[*+=–—─━═-](?:[ \t]|$)|(?:[*+=–—─━═-][ \t]*){2,}[ \t]*$)/;
|
|
966
939
|
|
|
967
|
-
/** @internal exported for tests — the grown-line
|
|
940
|
+
/** @internal exported for tests — the grown-line block-kind gate. */
|
|
968
941
|
export function fastLineStartHazard(grownLine: string): boolean {
|
|
969
|
-
return FAST_LINE_START_HAZARD_RE.test(grownLine) || REF_DEF_LINE_RE.test(grownLine);
|
|
942
|
+
return FAST_LINE_START_HAZARD_RE.test(grownLine) || REF_DEF_LINE_RE.test(grownLine) || grownLine.includes("\\end{");
|
|
970
943
|
}
|
|
971
944
|
|
|
972
945
|
/** Seam hazards between the captured raw row tail and the delta: the row must
|
|
@@ -1038,105 +1011,87 @@ function lexInlineTokens(text: string): Token[] {
|
|
|
1038
1011
|
const REF_DEF_LINE_RE = /^ {0,3}\[(?:\\.|[^\]\\])+\]:/;
|
|
1039
1012
|
const HAS_REF_DEF = new RegExp(REF_DEF_LINE_RE.source, "m");
|
|
1040
1013
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
// sits at the start of a top-level list token's raw:
|
|
1048
|
-
const LIST_MARKER_RE = /^ {0,3}(?:([*+-])|\d{1,9}([.)]))/;
|
|
1049
|
-
|
|
1050
|
-
// Streaming-freeze equivalence invariant: lex(prefix) ++ lex(tail) must equal
|
|
1051
|
-
// lex(full text) — for the CURRENT text and for every append-only extension of
|
|
1052
|
-
// it, because a frozen prefix is sticky (it keeps being reused while the text
|
|
1053
|
-
// grows). At a blank-line (`\n\n`) cut directly after a top-level `list`
|
|
1054
|
-
// token, the only construct that can straddle the cut is a continuation item
|
|
1055
|
-
// of that list: marked consumed the blank line into the last item's raw and
|
|
1056
|
-
// re-ran `listItemRegex` at exactly `tailStart`, merging a same-marker item
|
|
1057
|
-
// into one renumbered loose list. The cut is safe only when that regex can
|
|
1058
|
-
// NEVER match at `tailStart`, no matter what is appended later.
|
|
1059
|
-
//
|
|
1060
|
-
// Append-only growth means existing characters are immutable while new ones
|
|
1061
|
-
// may appear after them, so "closed" may only be concluded from a present
|
|
1062
|
-
// character that contradicts every possible continuation (e.g. tail "1x" can
|
|
1063
|
-
// never grow into an ordered item, but tail "1" can become "1. c"). Running
|
|
1064
|
-
// out of text mid-marker therefore answers "may continue".
|
|
1065
|
-
//
|
|
1066
|
-
// Returns true when the tail could still continue the list (or the list's
|
|
1067
|
-
// marker is unrecognizable) — the conservative "don't freeze" answer. marked
|
|
1068
|
-
// may break the list anyway when the matching line is also an hr (`- - -`);
|
|
1069
|
-
// treating that as "may continue" merely skips a freeze, never corrupts one.
|
|
1070
|
-
function listMayContinueAt(text: string, tailStart: number, listRaw: string): boolean {
|
|
1071
|
-
const marker = LIST_MARKER_RE.exec(listRaw);
|
|
1072
|
-
if (marker === null) return true; // unrecognized list shape — stay conservative
|
|
1073
|
-
const n = text.length;
|
|
1074
|
-
let i = tailStart;
|
|
1075
|
-
// `listItemRegex` allows up to 3 leading spaces (the caller's next-char
|
|
1076
|
-
// guard rejects whitespace at the final cut, but mirror the rule exactly).
|
|
1077
|
-
while (i < n && i - tailStart < 3 && text.charCodeAt(i) === 0x20 /* space */) i++;
|
|
1078
|
-
if (i >= n) return true;
|
|
1079
|
-
const bullet = marker[1];
|
|
1080
|
-
if (bullet !== undefined) {
|
|
1081
|
-
if (text[i] !== bullet) return false; // wrong marker char — closed forever
|
|
1082
|
-
i++;
|
|
1083
|
-
} else {
|
|
1084
|
-
// Ordered: 1-9 digits, then the same `.`/`)` delimiter.
|
|
1085
|
-
let digits = 0;
|
|
1086
|
-
while (i < n && digits < 10) {
|
|
1087
|
-
const c = text.charCodeAt(i);
|
|
1088
|
-
if (c < 0x30 /* 0 */ || c > 0x39 /* 9 */) break;
|
|
1089
|
-
digits++;
|
|
1090
|
-
i++;
|
|
1091
|
-
}
|
|
1092
|
-
if (digits === 0 || digits > 9) return false; // no digit run / too long — closed forever
|
|
1093
|
-
if (i >= n) return true; // delimiter (or more digits) may still arrive
|
|
1094
|
-
if (text[i] !== marker[2]) return false; // wrong delimiter — closed forever
|
|
1095
|
-
i++;
|
|
1096
|
-
}
|
|
1097
|
-
// After the marker: `(?:[\t ][^\n]*)?(?:\n|$)` — tab/space + anything, a
|
|
1098
|
-
// bare newline, or end-of-input (which appends can still extend).
|
|
1099
|
-
if (i >= n) return true;
|
|
1100
|
-
const after = text.charCodeAt(i);
|
|
1101
|
-
return after === 0x20 /* space */ || after === 0x09 /* tab */ || after === 0x0a; /* \n */
|
|
1014
|
+
/** The last stable block boundary of a token run: see {@link stableBlockBoundary}. */
|
|
1015
|
+
interface BlockBoundary {
|
|
1016
|
+
/** Offset just past the boundary token, or 0 when the run holds none. */
|
|
1017
|
+
end: number;
|
|
1018
|
+
/** Number of tokens up to and including the boundary token, or 0. */
|
|
1019
|
+
count: number;
|
|
1102
1020
|
}
|
|
1103
1021
|
|
|
1104
|
-
|
|
1022
|
+
// A whitespace-only line, capturing its terminator: "\n", or "" at the end of the text.
|
|
1023
|
+
const WHITESPACE_LINE_RE = /[^\S\n]*(\n|$)/y;
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* A display-math opener in the frozen streaming prefix whose block an append
|
|
1027
|
+
* could still close: the first such opener of its kind.
|
|
1028
|
+
*/
|
|
1029
|
+
interface PrefixOpener {
|
|
1030
|
+
kind: MathBlockOpener;
|
|
1031
|
+
/** Offset of the opener's line. */
|
|
1032
|
+
at: number;
|
|
1033
|
+
/** The stable boundary in front of the opener, as a text length and a token count. */
|
|
1034
|
+
rewindEnd: number;
|
|
1035
|
+
rewindCount: number;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const NO_OPENERS: readonly PrefixOpener[] = [];
|
|
1105
1039
|
|
|
1106
1040
|
/**
|
|
1107
1041
|
* Offset just past the last token in `tokens` that closes a block on a hard
|
|
1108
1042
|
* `"\n\n"` break, together with the number of tokens up to and including it.
|
|
1109
1043
|
* `count === 0` means the run holds no usable boundary.
|
|
1110
1044
|
*
|
|
1111
|
-
* `base` is where `tokens[0]` starts inside `text
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1045
|
+
* `base` is where `tokens[0]` starts inside `text`; `tokens` lex the rest of
|
|
1046
|
+
* `text`, which appends may still extend (the streaming freeze). A boundary
|
|
1047
|
+
* qualifies only when splitting there is invisible to the lexer, i.e.
|
|
1048
|
+
* `lex(head) ++ lex(tail) === lex(text)`:
|
|
1114
1049
|
* - The break must sit inside `text`. At end-of-text the next character is
|
|
1115
1050
|
* unknown (and, while streaming, may still arrive), so the cut is deferred.
|
|
1116
|
-
* - The next
|
|
1117
|
-
* block separator straddles the cut — e.g. a fence followed by
|
|
1118
|
-
* `"\n\n\n- list"` — and the two lexes desync.
|
|
1051
|
+
* - The next line must start real block content. A leading space or newline
|
|
1052
|
+
* means the block separator straddles the cut — e.g. a fence followed by
|
|
1053
|
+
* `"\n\n\n- list"` — and the two lexes desync. So does a line of any other
|
|
1054
|
+
* whitespace, such as a no-break space: the lexer's blank line is
|
|
1055
|
+
* `/^\s*\n$/`, so it joins the blank run in front of the cut. While
|
|
1056
|
+
* streaming, a whitespace-only last line may still become one.
|
|
1119
1057
|
* - A preceding `list` must be provably closed: CommonMark lets a same-marker
|
|
1120
1058
|
* item continue the list across the blank line, and marked merges both into
|
|
1121
1059
|
* one renumbered loose list (`listMayContinueAt`).
|
|
1060
|
+
* - Display math: `tokens` are the one-pass lex of `text` as it stands, so
|
|
1061
|
+
* a block the lex already made is a `math` token, and one that an
|
|
1062
|
+
* append could still close becomes one only once a closer line arrives,
|
|
1063
|
+
* which Markdown#lexTokens watches for. With `settle`, the scan instead
|
|
1064
|
+
* stops at a token whose display-math block an append could still close
|
|
1065
|
+
* (`mathBlockMayCloseAt`), so no append changes the tokens before the
|
|
1066
|
+
* boundary it finds. A pair whose closer line has already ended around a
|
|
1067
|
+
* whitespace-only body (`$$`, ` `, `$$`) is no block whatever follows, so
|
|
1068
|
+
* that scan goes on past it.
|
|
1122
1069
|
*
|
|
1123
1070
|
* `startIndex` resumes the scan at `tokens[startIndex]` (positions still
|
|
1124
1071
|
* accumulate from `base`). The streaming freeze passes the frozen-prefix
|
|
1125
1072
|
* token count: that prefix's boundary is permanent under append-only growth
|
|
1126
1073
|
* (re-verified when frozen), so only the mutable tail can hold a new one.
|
|
1074
|
+
* `endIndex` ends the scan in front of `tokens[endIndex]`, for the last
|
|
1075
|
+
* boundary in front of a given token.
|
|
1127
1076
|
*/
|
|
1128
1077
|
function stableBlockBoundary(
|
|
1129
1078
|
text: string,
|
|
1130
1079
|
base: number,
|
|
1131
1080
|
tokens: Token[],
|
|
1132
|
-
|
|
1133
|
-
|
|
1081
|
+
{
|
|
1082
|
+
startIndex = 0,
|
|
1083
|
+
endIndex = tokens.length,
|
|
1084
|
+
settle = false,
|
|
1085
|
+
}: { startIndex?: number; endIndex?: number; settle?: boolean } = {},
|
|
1086
|
+
): BlockBoundary {
|
|
1134
1087
|
let pos = base;
|
|
1135
1088
|
let end = 0;
|
|
1136
1089
|
let count = 0;
|
|
1137
|
-
for (let i = startIndex; i <
|
|
1138
|
-
const
|
|
1090
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
1091
|
+
const token = tokens[i];
|
|
1092
|
+
const raw = token.raw;
|
|
1139
1093
|
const tokenEnd = pos + raw.length;
|
|
1094
|
+
if (settle && token.type !== "math" && mathBlockMayCloseAt(text, pos)) break;
|
|
1140
1095
|
if (raw.endsWith("\n\n")) {
|
|
1141
1096
|
const prev = i > 0 ? tokens[i - 1] : undefined;
|
|
1142
1097
|
if (prev === undefined || prev.type !== "list" || !listMayContinueAt(text, tokenEnd, prev.raw)) {
|
|
@@ -1146,84 +1101,38 @@ function stableBlockBoundary(
|
|
|
1146
1101
|
}
|
|
1147
1102
|
pos = tokenEnd;
|
|
1148
1103
|
}
|
|
1149
|
-
if (count === 0 || end >= text.length) return
|
|
1104
|
+
if (count === 0 || end >= text.length) return { end: 0, count: 0 };
|
|
1150
1105
|
const next = text.charCodeAt(end);
|
|
1151
|
-
if (next === 0x20 /* space */ || next === 0x0a /* \n */) return
|
|
1106
|
+
if (next === 0x20 /* space */ || next === 0x0a /* \n */) return { end: 0, count: 0 };
|
|
1107
|
+
WHITESPACE_LINE_RE.lastIndex = end;
|
|
1108
|
+
if (WHITESPACE_LINE_RE.test(text)) return { end: 0, count: 0 };
|
|
1152
1109
|
return { end, count };
|
|
1153
1110
|
}
|
|
1154
1111
|
|
|
1155
|
-
// Bun's regex engine skips the start-anchor optimization for several of marked's
|
|
1156
|
-
// block rules — `hr`, `lheading`, `table` and `html` are `^`-anchored
|
|
1157
|
-
// alternations of quantified branches — so each failing `exec` rescans the whole
|
|
1158
|
-
// remaining source instead of stopping at offset 0. Lexing is then quadratic in
|
|
1159
|
-
// document length: an 800 KB message costs ~41 s under Bun where Node/V8 needs
|
|
1160
|
-
// ~60 ms, and it runs on the render path, freezing the UI. Bounded windows keep
|
|
1161
|
-
// every scan short and restore linear behavior (~0.7 s for that same message).
|
|
1162
|
-
const LEX_WINDOW_BYTES = 2 * 1024;
|
|
1163
|
-
// Under this size a single pass beats probing for window boundaries; the
|
|
1164
|
-
// crossover measured on pathological Markdown sits around 16 KB.
|
|
1165
|
-
const WINDOWED_LEX_MIN_BYTES = 16 * 1024;
|
|
1166
|
-
|
|
1167
1112
|
/**
|
|
1168
|
-
* Lex
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
* Window cuts come from marked itself: a throwaway BLOCK-ONLY probe lex of the
|
|
1172
|
-
* window reports its last stable block boundary ({@link stableBlockBoundary})
|
|
1173
|
-
* and only that confirmed segment is handed to the real lexer; a window
|
|
1174
|
-
* holding no boundary doubles until it finds one or reaches the end. Probes
|
|
1175
|
-
* never run inline tokenization (their inlineQueue is discarded) — a boundary
|
|
1176
|
-
* is a property of block structure alone, and probe inline passes were the
|
|
1177
|
-
* dominant cost of an earlier revision. Block tokenization runs per window
|
|
1178
|
-
* while inline tokenization is deferred to the end — mirroring `Lexer.lex` —
|
|
1179
|
-
* so a `[label]: dest` definition anywhere in the document still resolves for
|
|
1180
|
-
* every inline span.
|
|
1181
|
-
*
|
|
1182
|
-
* A boundary requires some top-level token whose raw ends in `"\n\n"`, so a
|
|
1183
|
-
* window that contains no blank line cannot cut: each round starts at the next
|
|
1184
|
-
* `"\n\n"` (skipping straight to the end when there is none — e.g. a tail
|
|
1185
|
-
* that is one long tight list) instead of probing sizes that cannot succeed.
|
|
1113
|
+
* Lex a whole document with the renderer's rules (math, custom rules,
|
|
1114
|
+
* strikethrough). Callers that split a document along its blocks (table
|
|
1115
|
+
* charts) lex through this so they see the blocks Markdown renders.
|
|
1186
1116
|
*/
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
let offset = 0;
|
|
1190
|
-
while (offset < text.length) {
|
|
1191
|
-
let segment = "";
|
|
1192
|
-
const nextBlank = text.indexOf("\n\n", offset);
|
|
1193
|
-
if (nextBlank === -1) {
|
|
1194
|
-
segment = text.slice(offset);
|
|
1195
|
-
} else {
|
|
1196
|
-
const minSize = Math.max(LEX_WINDOW_BYTES, nextBlank + 2 - offset);
|
|
1197
|
-
for (let size = minSize; segment.length === 0; size *= 2) {
|
|
1198
|
-
if (offset + size >= text.length) {
|
|
1199
|
-
segment = text.slice(offset);
|
|
1200
|
-
break;
|
|
1201
|
-
}
|
|
1202
|
-
const probe = new Lexer(markdownParser.defaults);
|
|
1203
|
-
probe.blockTokens(text.slice(offset, offset + size), probe.tokens);
|
|
1204
|
-
const boundary = stableBlockBoundary(text, offset, probe.tokens);
|
|
1205
|
-
if (boundary.count > 0) segment = text.slice(offset, boundary.end);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
lexer.blockTokens(segment, lexer.tokens);
|
|
1209
|
-
offset += segment.length;
|
|
1210
|
-
}
|
|
1211
|
-
for (const queued of lexer.inlineQueue) lexer.inlineTokens(queued.src, queued.tokens);
|
|
1212
|
-
lexer.inlineQueue = [];
|
|
1213
|
-
return lexer.tokens;
|
|
1117
|
+
export function lexDocument(text: string): TokensList {
|
|
1118
|
+
return markdownParser.lexer(text);
|
|
1214
1119
|
}
|
|
1215
1120
|
|
|
1216
|
-
/**
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* `source` as a fenced code block in `lang`: the fence is one backtick longer
|
|
1123
|
+
* than any backtick run in the source (at least three), so the block always
|
|
1124
|
+
* closes where intended. `open` leaves it unclosed, the source verbatim, for
|
|
1125
|
+
* a body that is still arriving.
|
|
1126
|
+
*/
|
|
1127
|
+
export function fencedCode(lang: string, source: string, options?: { open?: boolean }): string {
|
|
1128
|
+
const longest = source.match(/`+/g)?.reduce((max, run) => Math.max(max, run.length), 2) ?? 2;
|
|
1129
|
+
const fence = "`".repeat(longest + 1);
|
|
1130
|
+
return options?.open ? `${fence}${lang}\n${source}` : `${fence}${lang}\n${source.trimEnd()}\n${fence}`;
|
|
1222
1131
|
}
|
|
1223
1132
|
|
|
1224
1133
|
/** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
|
|
1225
1134
|
export interface MarkdownLink {
|
|
1226
|
-
/**
|
|
1135
|
+
/** Flattened visible label with whitespace collapsed to one row; falls back to `href` when empty. */
|
|
1227
1136
|
text: string;
|
|
1228
1137
|
/** Destination exactly as marked resolved it (references resolved, no normalization). */
|
|
1229
1138
|
href: string;
|
|
@@ -1261,10 +1170,124 @@ export function extractMarkdownLinks(text: string): MarkdownLink[] {
|
|
|
1261
1170
|
if (any.rows) for (const row of any.rows) for (const cell of row) walk(cell.tokens);
|
|
1262
1171
|
}
|
|
1263
1172
|
};
|
|
1264
|
-
walk(
|
|
1173
|
+
walk(markdownParser.lexer(text));
|
|
1265
1174
|
return links;
|
|
1266
1175
|
}
|
|
1267
1176
|
|
|
1177
|
+
/**
|
|
1178
|
+
* Offset just past the destination that starts at or after `start` in `raw`
|
|
1179
|
+
* (CommonMark link destination grammar), plus where it begins. `null` when no
|
|
1180
|
+
* destination is there.
|
|
1181
|
+
*/
|
|
1182
|
+
function scanLinkDestination(raw: string, start: number): { start: number; end: number } | null {
|
|
1183
|
+
let i = start;
|
|
1184
|
+
while (i < raw.length && (raw[i] === " " || raw[i] === "\t" || raw[i] === "\n" || raw[i] === "\r")) i++;
|
|
1185
|
+
if (i >= raw.length) return null;
|
|
1186
|
+
if (raw[i] === "<") {
|
|
1187
|
+
for (let j = i + 1; j < raw.length; j++) {
|
|
1188
|
+
if (raw[j] === "\\") j++;
|
|
1189
|
+
else if (raw[j] === ">") return { start: i, end: j + 1 };
|
|
1190
|
+
else if (raw[j] === "<" || raw[j] === "\n") return null;
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
let depth = 0;
|
|
1195
|
+
let j = i;
|
|
1196
|
+
for (; j < raw.length; j++) {
|
|
1197
|
+
const ch = raw[j]!;
|
|
1198
|
+
if (ch === "\\") {
|
|
1199
|
+
j++;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch.charCodeAt(0) < 0x20) break;
|
|
1203
|
+
if (ch === "(") depth++;
|
|
1204
|
+
else if (ch === ")") {
|
|
1205
|
+
if (depth === 0) break;
|
|
1206
|
+
depth--;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return j > i ? { start: i, end: j } : null;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** A destination that stays one destination whatever bytes the target holds. */
|
|
1213
|
+
function formatLinkDestination(target: string): string {
|
|
1214
|
+
return /[\s()<>\\]/.test(target) ? `<${target.replaceAll(/[<>\\]/g, ch => encodeURIComponent(ch))}>` : target;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function isListToken(token: Token): token is Tokens.List {
|
|
1218
|
+
return token.type === "list";
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function isTableToken(token: Token): token is Tokens.Table {
|
|
1222
|
+
return token.type === "table";
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Rewrite the destinations of inline links and link reference definitions in
|
|
1227
|
+
* `text` to `resolve(href)`, leaving everything else byte-for-byte intact.
|
|
1228
|
+
* Hosts that render the Markdown source themselves (a native terminal's `md`
|
|
1229
|
+
* node) resolve relative destinations against their own idea of the working
|
|
1230
|
+
* directory; handing them the session-resolved target keeps links pointing at
|
|
1231
|
+
* what the author meant. Links inside code, images, autolinks and reference
|
|
1232
|
+
* uses (`[x][ref]`, rewritten through their definition) are untouched; a link
|
|
1233
|
+
* whose source cannot be located exactly is left as written.
|
|
1234
|
+
*/
|
|
1235
|
+
export function rewriteMarkdownLinkDestinations(text: string, resolve: (href: string) => string | undefined): string {
|
|
1236
|
+
const edits: Array<{ start: number; end: number; target: string }> = [];
|
|
1237
|
+
let cursor = 0;
|
|
1238
|
+
const rewrite = (token: Token, labelEnd: number | undefined): void => {
|
|
1239
|
+
const at = text.indexOf(token.raw, cursor);
|
|
1240
|
+
if (at < 0) return;
|
|
1241
|
+
cursor = at + token.raw.length;
|
|
1242
|
+
const href = "href" in token && typeof token.href === "string" ? token.href : "";
|
|
1243
|
+
if (!href || labelEnd === undefined) return;
|
|
1244
|
+
const target = resolve(href);
|
|
1245
|
+
if (!target || target === href) return;
|
|
1246
|
+
const dest = scanLinkDestination(token.raw, labelEnd);
|
|
1247
|
+
if (dest) edits.push({ start: at + dest.start, end: at + dest.end, target });
|
|
1248
|
+
};
|
|
1249
|
+
const walk = (tokens: readonly Token[] | undefined): void => {
|
|
1250
|
+
if (!tokens) return;
|
|
1251
|
+
for (const token of tokens) {
|
|
1252
|
+
if (token.type === "link") {
|
|
1253
|
+
const label = "text" in token && typeof token.text === "string" ? `[${token.text}](` : undefined;
|
|
1254
|
+
rewrite(token, label && token.raw.startsWith(label) ? label.length : undefined);
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (token.type === "def") {
|
|
1258
|
+
const close = token.raw.indexOf("]:");
|
|
1259
|
+
rewrite(token, close < 0 ? undefined : close + 2);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const children = "tokens" in token && Array.isArray(token.tokens) ? token.tokens : undefined;
|
|
1263
|
+
const items = isListToken(token) ? token.items : undefined;
|
|
1264
|
+
const table = isTableToken(token) ? token : undefined;
|
|
1265
|
+
if (token.type === "image" || (!children && !items && !table)) {
|
|
1266
|
+
// A leaf (text, code span, fenced code, html, image…): step past it so a
|
|
1267
|
+
// later link's source is never matched inside it.
|
|
1268
|
+
const at = token.raw ? text.indexOf(token.raw, cursor) : -1;
|
|
1269
|
+
if (at >= 0) cursor = at + token.raw.length;
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
walk(children);
|
|
1273
|
+
walk(items);
|
|
1274
|
+
if (table) {
|
|
1275
|
+
for (const cell of table.header) walk(cell.tokens);
|
|
1276
|
+
for (const row of table.rows) for (const cell of row) walk(cell.tokens);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
walk(markdownParser.lexer(text));
|
|
1281
|
+
if (edits.length === 0) return text;
|
|
1282
|
+
let out = "";
|
|
1283
|
+
let last = 0;
|
|
1284
|
+
for (const edit of edits) {
|
|
1285
|
+
out += text.slice(last, edit.start) + formatLinkDestination(edit.target);
|
|
1286
|
+
last = edit.end;
|
|
1287
|
+
}
|
|
1288
|
+
return out + text.slice(last);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1268
1291
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
1269
1292
|
export function clearRenderCache(): void {
|
|
1270
1293
|
renderCache.clear();
|
|
@@ -1290,6 +1313,13 @@ function objectId(o: object): number {
|
|
|
1290
1313
|
export interface DefaultTextStyle {
|
|
1291
1314
|
/** Foreground color function */
|
|
1292
1315
|
color?: (text: string) => string;
|
|
1316
|
+
/**
|
|
1317
|
+
* The `color` foreground is prose-level: semantically-themed markdown spans
|
|
1318
|
+
* (headings, links) keep their own `md*` theme colors instead of being
|
|
1319
|
+
* painted over by it, while plain text runs still use `color`. Unset keeps
|
|
1320
|
+
* the historical behavior where `color` paints every run.
|
|
1321
|
+
*/
|
|
1322
|
+
proseFg?: boolean;
|
|
1293
1323
|
/** Background color function */
|
|
1294
1324
|
bgColor?: (text: string) => string;
|
|
1295
1325
|
/** Bold text */
|
|
@@ -1355,18 +1385,42 @@ export interface MarkdownTheme {
|
|
|
1355
1385
|
* Return null to fall back to fenced code rendering.
|
|
1356
1386
|
*/
|
|
1357
1387
|
resolveMermaidAscii?: (source: string, maxWidth?: number) => string | null;
|
|
1358
|
-
|
|
1388
|
+
/**
|
|
1389
|
+
* Glyphs for quote borders, rules, tables and color swatches. Optional so themes
|
|
1390
|
+
* built to the upstream pi-tui `MarkdownTheme` shape (which has no `symbols`)
|
|
1391
|
+
* still render; omitted symbols fall back to the active theme's set.
|
|
1392
|
+
*/
|
|
1393
|
+
symbols?: SymbolTheme;
|
|
1359
1394
|
}
|
|
1360
1395
|
|
|
1361
1396
|
interface InlineStyleContext {
|
|
1362
1397
|
applyText: (text: string) => string;
|
|
1363
1398
|
stylePrefix: string;
|
|
1399
|
+
/** True when the context came from the component's own default style (vs. a caller-supplied one). */
|
|
1400
|
+
isDefault?: boolean;
|
|
1364
1401
|
}
|
|
1365
1402
|
|
|
1366
|
-
type ListToken = Token & {
|
|
1403
|
+
type ListToken = Token & {
|
|
1404
|
+
items: Array<{ raw: string; tokens?: Token[] }>;
|
|
1405
|
+
ordered: boolean;
|
|
1406
|
+
start?: number;
|
|
1407
|
+
loose: boolean;
|
|
1408
|
+
};
|
|
1367
1409
|
type TableCellToken = { tokens?: Token[] };
|
|
1368
1410
|
type TableToken = Token & { header: TableCellToken[]; rows: TableCellToken[][]; raw?: string };
|
|
1369
1411
|
|
|
1412
|
+
/** `item` as marked lexes it inside a loose list: tight `text` blocks become paragraphs. */
|
|
1413
|
+
function loosenListItem(item: Tokens.ListItem): Tokens.ListItem {
|
|
1414
|
+
if (item.loose) return item;
|
|
1415
|
+
return {
|
|
1416
|
+
...item,
|
|
1417
|
+
loose: true,
|
|
1418
|
+
tokens: item.tokens.map(token =>
|
|
1419
|
+
token.type === "text" ? ({ ...token, type: "paragraph" } as Tokens.Paragraph) : token,
|
|
1420
|
+
),
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1370
1424
|
function formatHyperlink(text: string, target: string): string {
|
|
1371
1425
|
if (!TERMINAL.hyperlinks || !target) {
|
|
1372
1426
|
return text;
|
|
@@ -1631,6 +1685,15 @@ interface RenderSignature {
|
|
|
1631
1685
|
textSizing: boolean;
|
|
1632
1686
|
bgColorProbe: string;
|
|
1633
1687
|
headingProbe: string;
|
|
1688
|
+
/** Fallback glyphs for themes without `symbols`; their identity alone cannot tell presets apart. */
|
|
1689
|
+
symbolsProbe: string;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/** A shorter prefix the cached rows grew from: its length, its token count and the row count its rows end at. */
|
|
1693
|
+
interface PrefixMark {
|
|
1694
|
+
textEnd: number;
|
|
1695
|
+
tokenCount: number;
|
|
1696
|
+
lineEnd: number;
|
|
1634
1697
|
}
|
|
1635
1698
|
|
|
1636
1699
|
interface StreamPrefixLineCache extends RenderSignature {
|
|
@@ -1639,6 +1702,34 @@ interface StreamPrefixLineCache extends RenderSignature {
|
|
|
1639
1702
|
// Private to the cache and never handed to callers (each frame copies it
|
|
1640
1703
|
// into a fresh output array), so an advancing prefix appends in place.
|
|
1641
1704
|
lines: string[];
|
|
1705
|
+
// The shorter prefixes the rows grew from, oldest first. Each ends on a
|
|
1706
|
+
// frozen block boundary, so its rows are a render of its tokens alone,
|
|
1707
|
+
// and a rewound prefix keeps the rows of the last one it still covers.
|
|
1708
|
+
marks: PrefixMark[];
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* `cache` cut back to the rows of its last prefix of at most `tokenCount`
|
|
1713
|
+
* tokens, in fresh arrays, or `undefined` when it has none. A put-aside prefix
|
|
1714
|
+
* may still hold `cache`, so its arrays are never shared.
|
|
1715
|
+
*/
|
|
1716
|
+
function rewoundLineCache(
|
|
1717
|
+
cache: StreamPrefixLineCache | undefined,
|
|
1718
|
+
tokenCount: number,
|
|
1719
|
+
): StreamPrefixLineCache | undefined {
|
|
1720
|
+
if (cache === undefined) return undefined;
|
|
1721
|
+
if (cache.tokenCount <= tokenCount) return { ...cache, lines: cache.lines.slice(), marks: cache.marks.slice() };
|
|
1722
|
+
let i = cache.marks.length - 1;
|
|
1723
|
+
while (i >= 0 && cache.marks[i]!.tokenCount > tokenCount) i--;
|
|
1724
|
+
if (i < 0) return undefined;
|
|
1725
|
+
const mark = cache.marks[i]!;
|
|
1726
|
+
return {
|
|
1727
|
+
...cache,
|
|
1728
|
+
text: cache.text.slice(0, mark.textEnd),
|
|
1729
|
+
tokenCount: mark.tokenCount,
|
|
1730
|
+
lines: cache.lines.slice(0, mark.lineEnd),
|
|
1731
|
+
marks: cache.marks.slice(0, i),
|
|
1732
|
+
};
|
|
1642
1733
|
}
|
|
1643
1734
|
/**
|
|
1644
1735
|
* Per-token row cache for the *unfrozen tail* (PoC H). The tail re-lexes every
|
|
@@ -1679,6 +1770,25 @@ interface TailRowCache extends RenderSignature {
|
|
|
1679
1770
|
// type of token[i+1] when the rows were produced (blank/spacing gate).
|
|
1680
1771
|
nextTypes: (string | undefined)[];
|
|
1681
1772
|
}
|
|
1773
|
+
interface ListItemRowsCache {
|
|
1774
|
+
signature: RenderSignature;
|
|
1775
|
+
width: number;
|
|
1776
|
+
themeEpoch: number;
|
|
1777
|
+
ordered: boolean;
|
|
1778
|
+
start: number | undefined;
|
|
1779
|
+
loose: boolean;
|
|
1780
|
+
raws: string[];
|
|
1781
|
+
rows: RenderedLine[][];
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
interface StreamingListCache {
|
|
1785
|
+
text: string;
|
|
1786
|
+
before: Token[];
|
|
1787
|
+
start: number;
|
|
1788
|
+
tailStart: number;
|
|
1789
|
+
stableCount: number;
|
|
1790
|
+
list: Tokens.List;
|
|
1791
|
+
}
|
|
1682
1792
|
/**
|
|
1683
1793
|
* Mutable per-token record collector passed to #renderContentLines while
|
|
1684
1794
|
* rendering the streaming tail. The render loop fills `raws`/`nextTypes`
|
|
@@ -1691,6 +1801,17 @@ interface TailRenderRecorder {
|
|
|
1691
1801
|
raws: (string | undefined)[];
|
|
1692
1802
|
nextTypes: (string | undefined)[];
|
|
1693
1803
|
}
|
|
1804
|
+
/**
|
|
1805
|
+
* The frozen streaming prefix as it stood before a rewind whose only cause was
|
|
1806
|
+
* a closer on the still-growing last line, with the row caches keyed on it.
|
|
1807
|
+
*/
|
|
1808
|
+
interface RewoundPrefix {
|
|
1809
|
+
text: string;
|
|
1810
|
+
tokens: Token[];
|
|
1811
|
+
openers: readonly PrefixOpener[];
|
|
1812
|
+
lineCache: StreamPrefixLineCache | undefined;
|
|
1813
|
+
tailRowCache: TailRowCache | undefined;
|
|
1814
|
+
}
|
|
1694
1815
|
interface StreamingHighlightCache extends RenderSignature {
|
|
1695
1816
|
lang: string | undefined;
|
|
1696
1817
|
text: string;
|
|
@@ -1719,6 +1840,8 @@ export class Markdown implements Component {
|
|
|
1719
1840
|
#paddingY: number; // Top/bottom padding
|
|
1720
1841
|
#defaultTextStyle?: DefaultTextStyle;
|
|
1721
1842
|
#theme: MarkdownTheme;
|
|
1843
|
+
#symbols: SymbolTheme;
|
|
1844
|
+
#symbolsProbe: string;
|
|
1722
1845
|
#defaultStylePrefix?: string;
|
|
1723
1846
|
/** Number of spaces used to indent code block content. */
|
|
1724
1847
|
#codeBlockIndent: number;
|
|
@@ -1741,6 +1864,20 @@ export class Markdown implements Component {
|
|
|
1741
1864
|
#streamPrefixText?: string;
|
|
1742
1865
|
#streamPrefixTokens?: Token[];
|
|
1743
1866
|
#streamPrefixLineCache?: StreamPrefixLineCache;
|
|
1867
|
+
// The first display-math opener of each kind in the frozen prefix whose
|
|
1868
|
+
// block an append could still close. The prefix lexes them as the text
|
|
1869
|
+
// stands, which stays right until a tail line could close one of them
|
|
1870
|
+
// (mathBlockCloserIndex). Such a line turns the text from its kind's opener
|
|
1871
|
+
// on into one math block, so the prefix goes back to the boundary in front
|
|
1872
|
+
// of that opener, keeps the openers in front of it, and the rest is re-lexed.
|
|
1873
|
+
#streamPrefixOpeners: readonly PrefixOpener[] = NO_OPENERS;
|
|
1874
|
+
// The frozen prefix's leading run in front of the first of those openers:
|
|
1875
|
+
// no append can change its tokens, so getLastRenderStableText publishes it.
|
|
1876
|
+
#streamSettledText?: string;
|
|
1877
|
+
// The prefix before a rewind that only a closer on the still-growing last
|
|
1878
|
+
// line caused. Once a later text holds no closer line after it, that
|
|
1879
|
+
// prefix is right again, so #lexTokens puts it back.
|
|
1880
|
+
#streamRewound?: RewoundPrefix;
|
|
1744
1881
|
// Guard-scan memo (PoC C): the ref-def/CR verdict with the exact text
|
|
1745
1882
|
// length it was checked on. Reuse is sound only while setText has been
|
|
1746
1883
|
// append-only since (tracked via the startsWith that setText performs): a
|
|
@@ -1765,6 +1902,8 @@ export class Markdown implements Component {
|
|
|
1765
1902
|
// the blank-replacement branch of setText and the fallback branch of
|
|
1766
1903
|
// #lexTokens.
|
|
1767
1904
|
#tailRowCache?: TailRowCache;
|
|
1905
|
+
#listItemRowsCache?: ListItemRowsCache;
|
|
1906
|
+
#streamingListCache?: StreamingListCache;
|
|
1768
1907
|
// True while #renderStreamingContentLines renders the frozen token range:
|
|
1769
1908
|
// frozen code blocks highlight even in transient mode so their bytes match
|
|
1770
1909
|
// the finalized render (they render once into the prefix line cache, so
|
|
@@ -1800,6 +1939,9 @@ export class Markdown implements Component {
|
|
|
1800
1939
|
this.#paddingX = paddingX;
|
|
1801
1940
|
this.#paddingY = paddingY;
|
|
1802
1941
|
this.#theme = theme;
|
|
1942
|
+
this.#symbols = theme.symbols ?? getSymbolTheme();
|
|
1943
|
+
const { quoteBorder, hrChar, colorSwatch, table } = this.#symbols;
|
|
1944
|
+
this.#symbolsProbe = theme.symbols ? "" : [quoteBorder, hrChar, colorSwatch, ...Object.values(table)].join("");
|
|
1803
1945
|
this.#defaultTextStyle = defaultTextStyle;
|
|
1804
1946
|
this.#codeBlockIndent = Math.max(0, Math.floor(codeBlockIndent));
|
|
1805
1947
|
}
|
|
@@ -1854,8 +1996,13 @@ export class Markdown implements Component {
|
|
|
1854
1996
|
if (text === this.#text) return false;
|
|
1855
1997
|
if (!text.startsWith(this.#text)) {
|
|
1856
1998
|
// Non-append edit: the previous frame's guard verdict cannot be
|
|
1857
|
-
// reused — the checked region may have changed anywhere.
|
|
1999
|
+
// reused — the checked region may have changed anywhere. The frozen
|
|
2000
|
+
// prefix, or one a rewind set aside, may still start the new text,
|
|
2001
|
+
// but the line after it was replaced, and a boundary holds only
|
|
2002
|
+
// while that line starts a block of its own.
|
|
1858
2003
|
this.#appendOnlySinceLastScan = false;
|
|
2004
|
+
this.#dropStreamPrefix();
|
|
2005
|
+
this.#streamRewound = undefined;
|
|
1859
2006
|
}
|
|
1860
2007
|
this.#text = text;
|
|
1861
2008
|
if (!text.trim()) {
|
|
@@ -1863,6 +2010,7 @@ export class Markdown implements Component {
|
|
|
1863
2010
|
// the non-append edit, so drop the frozen stream state here or it
|
|
1864
2011
|
// outlives the content it indexed.
|
|
1865
2012
|
this.#dropStreamPrefix();
|
|
2013
|
+
this.#listItemRowsCache = undefined;
|
|
1866
2014
|
// B+: the captured fast-path rows index the replaced content — drop
|
|
1867
2015
|
// the recipe so a fresh stream cannot splice onto stale rows.
|
|
1868
2016
|
this.#fastTail = undefined;
|
|
@@ -1877,6 +2025,14 @@ export class Markdown implements Component {
|
|
|
1877
2025
|
this.#cachedLines = undefined;
|
|
1878
2026
|
}
|
|
1879
2027
|
|
|
2028
|
+
releaseRenderCaches(): void {
|
|
2029
|
+
this.invalidate();
|
|
2030
|
+
this.#dropStreamPrefix();
|
|
2031
|
+
this.#streamingHighlightCache = undefined;
|
|
2032
|
+
this.#fastTail = undefined;
|
|
2033
|
+
this.#lastTailCapture = undefined;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1880
2036
|
/**
|
|
1881
2037
|
* The unrendered source as an `md` node, `stream` while the streaming
|
|
1882
2038
|
* (transient) cache is on. Append-only growth yields a fresh node of the
|
|
@@ -1896,11 +2052,13 @@ export class Markdown implements Component {
|
|
|
1896
2052
|
|
|
1897
2053
|
/**
|
|
1898
2054
|
* Width-independent source prefix of the last render ending at a frozen
|
|
1899
|
-
* Markdown block boundary
|
|
1900
|
-
*
|
|
2055
|
+
* Markdown block boundary that no append can move: it stops in front of a
|
|
2056
|
+
* display-math opener whose block an append could still close. Only
|
|
2057
|
+
* meaningful while streaming (transient render cache on); grows
|
|
2058
|
+
* monotonically under append-only `setText`.
|
|
1901
2059
|
*/
|
|
1902
2060
|
getLastRenderStableText(): string {
|
|
1903
|
-
return this.#transientRenderCache ? (this.#
|
|
2061
|
+
return this.#transientRenderCache ? (this.#streamSettledText ?? "") : "";
|
|
1904
2062
|
}
|
|
1905
2063
|
|
|
1906
2064
|
get transientRenderCache(): boolean {
|
|
@@ -1930,7 +2088,10 @@ export class Markdown implements Component {
|
|
|
1930
2088
|
// render consumes it (see #lexTokens / the L2 hit path).
|
|
1931
2089
|
this.#streamPrefixLineCache = undefined;
|
|
1932
2090
|
this.#tailRowCache = undefined;
|
|
2091
|
+
this.#listItemRowsCache = undefined;
|
|
1933
2092
|
this.#streamingHighlightCache = undefined;
|
|
2093
|
+
this.#streamingListCache = undefined;
|
|
2094
|
+
this.#streamRewound = undefined;
|
|
1934
2095
|
}
|
|
1935
2096
|
this.invalidate();
|
|
1936
2097
|
}
|
|
@@ -1942,15 +2103,25 @@ export class Markdown implements Component {
|
|
|
1942
2103
|
// raw-span offsets). Every fallback is correctness-preserving — only speed
|
|
1943
2104
|
// differs; the render loop sees the identical token list either way.
|
|
1944
2105
|
#lexTokens(text: string): Token[] {
|
|
1945
|
-
//
|
|
1946
|
-
//
|
|
1947
|
-
//
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
2106
|
+
// A prefix is frozen only while canStream holds and its lex registered no
|
|
2107
|
+
// definitions, and it ends at a "\n\n" block boundary (stableBlockBoundary),
|
|
2108
|
+
// so the tail starts at a fresh line. The scan below then catches a
|
|
2109
|
+
// top-level definition or CR in the new text and the tail lex's links catch
|
|
2110
|
+
// a nested one, so the grown prefix is never re-scanned (O(n²) → O(n)).
|
|
2111
|
+
if (this.#streamRewound !== undefined) this.#restoreRewoundPrefix(text);
|
|
2112
|
+
const frozenText = this.#streamPrefixText;
|
|
2113
|
+
const frozenTokens = this.#streamPrefixTokens;
|
|
2114
|
+
const grewPastPrefix = frozenText !== undefined && text.length > frozenText.length && text.startsWith(frozenText);
|
|
2115
|
+
// A tail line that could close a display-math block the prefix holds open
|
|
2116
|
+
// makes the text from that block's opener on one `math` token, so the
|
|
2117
|
+
// prefix goes back to the boundary in front of the opener, and the rest
|
|
2118
|
+
// is lexed again. The check reads only the tail, which is lexed anyway.
|
|
2119
|
+
if (grewPastPrefix && frozenTokens !== undefined && this.#streamPrefixOpeners.length > 0) {
|
|
2120
|
+
this.#rewindForCloser(text, frozenText, frozenTokens);
|
|
2121
|
+
}
|
|
1950
2122
|
const prefix = this.#streamPrefixText;
|
|
1951
2123
|
const prefixTokens = this.#streamPrefixTokens;
|
|
1952
|
-
const hasPrefix =
|
|
1953
|
-
prefix !== undefined && prefixTokens !== undefined && text.length > prefix.length && text.startsWith(prefix);
|
|
2124
|
+
const hasPrefix = grewPastPrefix && prefix !== undefined && prefixTokens !== undefined;
|
|
1954
2125
|
const refDefText = hasPrefix ? text.slice(prefix.length) : text;
|
|
1955
2126
|
// Guard-scan memo (PoC C): while setText has been append-only and the
|
|
1956
2127
|
// grown delta introduces no "[", "]", ":", "\n" or "\r", the previous
|
|
@@ -1996,37 +2167,201 @@ export class Markdown implements Component {
|
|
|
1996
2167
|
}
|
|
1997
2168
|
this.#lastScanLength = text.length;
|
|
1998
2169
|
this.#lastScanCanStream = canStream;
|
|
2170
|
+
if (!canStream) this.#listItemRowsCache = undefined;
|
|
1999
2171
|
this.#lastScanValid = true;
|
|
2000
2172
|
this.#appendOnlySinceLastScan = true;
|
|
2173
|
+
if (canStream && retainPrefix) {
|
|
2174
|
+
const tokens = this.#resumeStreamingList(text);
|
|
2175
|
+
if (tokens !== undefined) {
|
|
2176
|
+
this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2177
|
+
this.#checkpointStreamingList(text, tokens);
|
|
2178
|
+
return tokens;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2001
2181
|
if (canStream && hasPrefix) {
|
|
2002
2182
|
const tailTokens = lexDocument(refDefText);
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2183
|
+
// A definition at any nesting depth registers for the whole document
|
|
2184
|
+
// and can resolve a reference in the frozen prefix, which was lexed
|
|
2185
|
+
// without it, so any definition in the tail sends the text to a full lex.
|
|
2186
|
+
if (Object.keys(tailTokens.links).length === 0) {
|
|
2187
|
+
const tokens = [...prefixTokens, ...tailTokens];
|
|
2188
|
+
if (retainPrefix) this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2189
|
+
else this.#dropStreamPrefix();
|
|
2190
|
+
if (retainPrefix) this.#checkpointStreamingList(text, tokens);
|
|
2191
|
+
return tokens;
|
|
2192
|
+
}
|
|
2007
2193
|
}
|
|
2008
2194
|
const tokens = lexDocument(text);
|
|
2009
|
-
|
|
2195
|
+
const hasReferenceDefinitions = Object.keys(tokens.links).length > 0;
|
|
2196
|
+
if (hasReferenceDefinitions) this.#lastScanCanStream = false;
|
|
2197
|
+
// A definition frozen into the prefix would be missing from every later
|
|
2198
|
+
// tail lex, so a full lex that registered any definition freezes nothing.
|
|
2199
|
+
if (canStream && retainPrefix && !hasReferenceDefinitions) {
|
|
2010
2200
|
this.#freezeStablePrefix(text, tokens, { preserveExisting: false });
|
|
2201
|
+
this.#checkpointStreamingList(text, tokens);
|
|
2011
2202
|
} else {
|
|
2012
2203
|
this.#dropStreamPrefix();
|
|
2013
2204
|
}
|
|
2014
2205
|
return tokens;
|
|
2015
2206
|
}
|
|
2016
2207
|
|
|
2017
|
-
|
|
2208
|
+
#resumeStreamingList(text: string): Token[] | undefined {
|
|
2209
|
+
const cached = this.#streamingListCache;
|
|
2210
|
+
if (cached === undefined || text.length <= cached.text.length || !text.startsWith(cached.text)) return undefined;
|
|
2211
|
+
const tail = lexDocument(text.slice(cached.tailStart));
|
|
2212
|
+
if (Object.keys(tail.links).length > 0) return undefined;
|
|
2213
|
+
const first = tail[0];
|
|
2214
|
+
if (first?.type !== "list") return undefined;
|
|
2215
|
+
const list = first as Tokens.List;
|
|
2216
|
+
if (!listMayContinueAt(list.raw, 0, cached.list.raw)) return undefined;
|
|
2217
|
+
const loose = cached.list.loose || list.loose;
|
|
2218
|
+
const completed = cached.list.items.slice(0, cached.stableCount);
|
|
2219
|
+
const items = [...completed, ...list.items];
|
|
2220
|
+
const merged: Tokens.List = {
|
|
2221
|
+
...cached.list,
|
|
2222
|
+
raw: text.slice(cached.start, cached.tailStart + list.raw.length),
|
|
2223
|
+
loose,
|
|
2224
|
+
items: loose ? items.map(loosenListItem) : items,
|
|
2225
|
+
};
|
|
2226
|
+
return [...cached.before, merged, ...tail.slice(1)];
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
#checkpointStreamingList(text: string, tokens: Token[]): void {
|
|
2230
|
+
const previous = this.#streamingListCache;
|
|
2231
|
+
this.#streamingListCache = undefined;
|
|
2232
|
+
let index = tokens.length - 1;
|
|
2233
|
+
while (tokens[index]?.type === "space") index--;
|
|
2234
|
+
const token = tokens[index];
|
|
2235
|
+
if (token?.type !== "list") return;
|
|
2236
|
+
const list = token as Tokens.List;
|
|
2237
|
+
let start = 0;
|
|
2238
|
+
for (let i = 0; i < index; i++) start += tokens[i]!.raw.length;
|
|
2239
|
+
// Late block delimiters can absorb an unfrozen preceding region. Bare
|
|
2240
|
+
// environments can also absorb list items through an equation prefix.
|
|
2241
|
+
if (start !== (this.#streamPrefixText?.length ?? 0) || BARE_ENV_BEGIN.test(list.raw)) return;
|
|
2242
|
+
const canResume = previous !== undefined && previous.start === start && text.startsWith(previous.text);
|
|
2243
|
+
let cursor = canResume ? previous.tailStart : start;
|
|
2244
|
+
let tailStart = start;
|
|
2245
|
+
let stableCount = 0;
|
|
2246
|
+
const indent = /^ */.exec(list.raw)![0].length;
|
|
2247
|
+
for (let i = canResume ? previous.stableCount : 0; i < list.items.length; i++) {
|
|
2248
|
+
const item = list.items[i]!;
|
|
2249
|
+
const position = text.indexOf(item.raw, cursor);
|
|
2250
|
+
if (position < cursor || position >= start + list.raw.length) return;
|
|
2251
|
+
// List continuation uses the first item's indentation. Resume only at
|
|
2252
|
+
// an item with that same indentation so the lexer sees the same context.
|
|
2253
|
+
if (/^ */.exec(item.raw)![0].length === indent) {
|
|
2254
|
+
tailStart = position;
|
|
2255
|
+
stableCount = i;
|
|
2256
|
+
}
|
|
2257
|
+
cursor = position + item.raw.length;
|
|
2258
|
+
}
|
|
2259
|
+
this.#streamingListCache = { text, before: tokens.slice(0, index), start, tailStart, stableCount, list };
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* Drop the frozen lex prefix and the transient row caches keyed on it. A
|
|
2264
|
+
* prefix set aside for #restoreRewoundPrefix checks itself against each
|
|
2265
|
+
* later text, so it stays until a non-append edit.
|
|
2266
|
+
*/
|
|
2018
2267
|
#dropStreamPrefix(): void {
|
|
2019
2268
|
this.#streamPrefixText = undefined;
|
|
2020
2269
|
this.#streamPrefixTokens = undefined;
|
|
2270
|
+
this.#streamPrefixOpeners = NO_OPENERS;
|
|
2271
|
+
this.#streamSettledText = undefined;
|
|
2021
2272
|
this.#streamPrefixLineCache = undefined;
|
|
2022
2273
|
this.#tailRowCache = undefined;
|
|
2274
|
+
this.#listItemRowsCache = undefined;
|
|
2275
|
+
this.#streamingListCache = undefined;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Rewind the frozen prefix `prefix` (its tokens `tokens`) for a line of the
|
|
2280
|
+
* tail after it that could close one of its openers: back to the boundary
|
|
2281
|
+
* in front of the first opener whose closer a tail line holds. When that
|
|
2282
|
+
* closer, and no other, is on the still-growing last line, the next chunk
|
|
2283
|
+
* can turn the line into text again, so the prefix is set aside for
|
|
2284
|
+
* #restoreRewoundPrefix.
|
|
2285
|
+
*/
|
|
2286
|
+
#rewindForCloser(text: string, prefix: string, tokens: Token[]): void {
|
|
2287
|
+
let target: PrefixOpener | undefined;
|
|
2288
|
+
let lastLineOnly = false;
|
|
2289
|
+
for (const opener of this.#streamPrefixOpeners) {
|
|
2290
|
+
const closer = mathBlockCloserIndex(text, prefix.length, opener.kind);
|
|
2291
|
+
if (closer === undefined) continue;
|
|
2292
|
+
lastLineOnly = target === undefined && !text.includes("\n", closer);
|
|
2293
|
+
target ??= opener;
|
|
2294
|
+
}
|
|
2295
|
+
if (target === undefined) return;
|
|
2296
|
+
this.#streamRewound = lastLineOnly
|
|
2297
|
+
? {
|
|
2298
|
+
text: prefix,
|
|
2299
|
+
tokens,
|
|
2300
|
+
openers: this.#streamPrefixOpeners,
|
|
2301
|
+
lineCache: this.#streamPrefixLineCache,
|
|
2302
|
+
tailRowCache: this.#tailRowCache,
|
|
2303
|
+
}
|
|
2304
|
+
: undefined;
|
|
2305
|
+
this.#rewindStreamPrefix(target.rewindEnd, target.rewindCount);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Cut the frozen prefix back to the boundary `end` characters and `count`
|
|
2310
|
+
* tokens in, keeping the openers in front of it and the rows of the prefix
|
|
2311
|
+
* that remains, and dropping the tail row cache keyed on the old prefix.
|
|
2312
|
+
*/
|
|
2313
|
+
#rewindStreamPrefix(end: number, count: number): void {
|
|
2314
|
+
this.#streamPrefixLineCache = rewoundLineCache(this.#streamPrefixLineCache, count);
|
|
2315
|
+
this.#tailRowCache = undefined;
|
|
2316
|
+
this.#listItemRowsCache = undefined;
|
|
2317
|
+
this.#streamingListCache = undefined;
|
|
2318
|
+
if (count === 0) {
|
|
2319
|
+
this.#streamPrefixText = undefined;
|
|
2320
|
+
this.#streamPrefixTokens = undefined;
|
|
2321
|
+
this.#streamPrefixOpeners = NO_OPENERS;
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
this.#streamPrefixText = this.#streamPrefixText?.slice(0, end);
|
|
2325
|
+
this.#streamPrefixTokens = this.#streamPrefixTokens?.slice(0, count);
|
|
2326
|
+
this.#streamPrefixOpeners = this.#streamPrefixOpeners.filter(opener => opener.at < end);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Put back the prefix #rewindForCloser set aside once no line of `text`
|
|
2331
|
+
* after it could close one of its openers: the last line it rewound for
|
|
2332
|
+
* has grown into text, so the prefix lexes as the text stands again. A
|
|
2333
|
+
* closer line that has ended stays one under every append, so a prefix
|
|
2334
|
+
* with one after it can never come back.
|
|
2335
|
+
*/
|
|
2336
|
+
#restoreRewoundPrefix(text: string): void {
|
|
2337
|
+
const rewound = this.#streamRewound;
|
|
2338
|
+
if (rewound === undefined) return;
|
|
2339
|
+
if (text.length <= rewound.text.length || !text.startsWith(rewound.text)) {
|
|
2340
|
+
this.#streamRewound = undefined;
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
for (const opener of rewound.openers) {
|
|
2344
|
+
const closer = mathBlockCloserIndex(text, rewound.text.length, opener.kind);
|
|
2345
|
+
if (closer === undefined) continue;
|
|
2346
|
+
if (text.includes("\n", closer)) this.#streamRewound = undefined;
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
this.#streamRewound = undefined;
|
|
2350
|
+
this.#streamPrefixText = rewound.text;
|
|
2351
|
+
this.#streamPrefixTokens = rewound.tokens;
|
|
2352
|
+
this.#streamPrefixOpeners = rewound.openers;
|
|
2353
|
+
this.#streamPrefixLineCache = rewound.lineCache;
|
|
2354
|
+
this.#tailRowCache = rewound.tailRowCache;
|
|
2355
|
+
this.#listItemRowsCache = undefined;
|
|
2356
|
+
this.#streamingListCache = undefined;
|
|
2023
2357
|
}
|
|
2024
2358
|
|
|
2025
2359
|
// Freeze the largest run of leading blocks that end on a hard "\n\n" boundary
|
|
2026
|
-
// (complete and immutable under append-only growth
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2360
|
+
// (complete, and immutable under append-only growth until a tail line could
|
|
2361
|
+
// close a display-math block it holds open) so the next streaming render
|
|
2362
|
+
// re-lexes only the unfrozen tail. Caller guarantees no CR / no reference
|
|
2363
|
+
// definitions, so each token's `raw` is a verbatim slice of `text` and the
|
|
2364
|
+
// summed offsets address `text` exactly.
|
|
2030
2365
|
#freezeStablePrefix(text: string, tokens: Token[], opts: { preserveExisting: boolean }): void {
|
|
2031
2366
|
// On the streaming-concat path (preserveExisting), tokens[0..prefixCount)
|
|
2032
2367
|
// ARE the previously frozen prefix and the text above it is byte-
|
|
@@ -2037,19 +2372,50 @@ export class Markdown implements Component {
|
|
|
2037
2372
|
// path (preserveExisting: false) re-derives the whole stream, so it
|
|
2038
2373
|
// must keep walking from 0.
|
|
2039
2374
|
const skipPrefix = opts.preserveExisting ? (this.#streamPrefixTokens?.length ?? 0) : 0;
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
skipPrefix,
|
|
2045
|
-
);
|
|
2046
|
-
if (frozen.count > 0) {
|
|
2047
|
-
this.#streamPrefixText = text.slice(0, frozen.end);
|
|
2048
|
-
this.#streamPrefixTokens = tokens.slice(0, frozen.count);
|
|
2375
|
+
const base = skipPrefix > 0 ? (this.#streamPrefixText?.length ?? 0) : 0;
|
|
2376
|
+
const frozen = stableBlockBoundary(text, base, tokens, { startIndex: skipPrefix });
|
|
2377
|
+
if (frozen.count === 0) {
|
|
2378
|
+
if (!opts.preserveExisting) this.#dropStreamPrefix();
|
|
2049
2379
|
return;
|
|
2050
2380
|
}
|
|
2051
|
-
|
|
2052
|
-
if (
|
|
2381
|
+
let openers = skipPrefix > 0 ? this.#streamPrefixOpeners : NO_OPENERS;
|
|
2382
|
+
if (openers.length === 0) {
|
|
2383
|
+
// The prefix so far is all settled, so the settled part grows up to the
|
|
2384
|
+
// last boundary in front of the first opener an append could close.
|
|
2385
|
+
const settled = stableBlockBoundary(text, base, tokens, { startIndex: skipPrefix, settle: true });
|
|
2386
|
+
if (settled.count > 0) {
|
|
2387
|
+
this.#streamSettledText = text.slice(0, settled.end);
|
|
2388
|
+
} else if (skipPrefix === 0) {
|
|
2389
|
+
this.#streamSettledText = undefined;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
// Record the first opener of each kind in the newly frozen tokens that an
|
|
2393
|
+
// append could still close, with the boundary a closer of its kind
|
|
2394
|
+
// rewinds to: the last one in front of it, or the prefix end when the new
|
|
2395
|
+
// tokens hold none. It is never in front of an earlier opener's, so the
|
|
2396
|
+
// first opener whose closer a tail line holds has the earliest boundary.
|
|
2397
|
+
let pos = base;
|
|
2398
|
+
for (let i = skipPrefix; i < frozen.count && openers.length < 2; i++) {
|
|
2399
|
+
const token = tokens[i];
|
|
2400
|
+
if (token.type !== "math") {
|
|
2401
|
+
const kind = mathBlockOpenerAt(text, pos);
|
|
2402
|
+
if (kind !== undefined && !openers.some(opener => opener.kind === kind) && mathBlockMayCloseAt(text, pos)) {
|
|
2403
|
+
const boundary = stableBlockBoundary(text, base, tokens, { startIndex: skipPrefix, endIndex: i });
|
|
2404
|
+
let rewindEnd = boundary.count > 0 ? boundary.end : base;
|
|
2405
|
+
let rewindCount = boundary.count > 0 ? boundary.count : skipPrefix;
|
|
2406
|
+
const earlier = openers.at(-1);
|
|
2407
|
+
if (earlier !== undefined && earlier.rewindEnd > rewindEnd) {
|
|
2408
|
+
rewindEnd = earlier.rewindEnd;
|
|
2409
|
+
rewindCount = earlier.rewindCount;
|
|
2410
|
+
}
|
|
2411
|
+
openers = [...openers, { kind, at: pos, rewindEnd, rewindCount }];
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
pos += token.raw.length;
|
|
2415
|
+
}
|
|
2416
|
+
this.#streamPrefixText = text.slice(0, frozen.end);
|
|
2417
|
+
this.#streamPrefixTokens = tokens.slice(0, frozen.count);
|
|
2418
|
+
this.#streamPrefixOpeners = openers;
|
|
2053
2419
|
}
|
|
2054
2420
|
|
|
2055
2421
|
render(width: number): readonly string[] {
|
|
@@ -2131,11 +2497,16 @@ export class Markdown implements Component {
|
|
|
2131
2497
|
(!markerDelta && recipe.rowRaw.endsWith("$") && /^[0-9]/.test(deltaTabs));
|
|
2132
2498
|
// A delta opening a pairing char when the captured row ENDS with the
|
|
2133
2499
|
// same char can re-pair across the seam: cold lex of the joined run
|
|
2134
|
-
// makes ONE token (x *a**b* → em("a**b")), the splice keeps two.
|
|
2135
|
-
//
|
|
2500
|
+
// makes ONE token (x *a**b* → em("a**b")), the splice keeps two. So
|
|
2501
|
+
// can an emphasis delimiter in the delta and the same char anywhere
|
|
2502
|
+
// in the row, once the joined text makes the row's one flanking
|
|
2503
|
+
// (`a_{3` + `} + b_{` renders `{3} + b` emphasized): the delta lexed
|
|
2504
|
+
// alone has nothing to pair with. An image marker (`x!` + `[a](u)`)
|
|
2505
|
+
// re-pairs the same way.
|
|
2136
2506
|
const pairSeamHazard =
|
|
2137
2507
|
markerDelta &&
|
|
2138
2508
|
((/^[*~`]/.test(deltaTabs) && /[*~`]$/.test(recipe.rowRaw)) ||
|
|
2509
|
+
FAST_EMPHASIS_DELIMITERS.some(c => deltaTabs.includes(c) && recipe.rowRaw.includes(c)) ||
|
|
2139
2510
|
// "x!" + "[a](u)": cold lexes text("x") + image(alt); the splice would
|
|
2140
2511
|
// keep "x!" + a styled link byte-run.
|
|
2141
2512
|
(deltaTabs.startsWith("[") && recipe.rowRaw.endsWith("!")));
|
|
@@ -2161,7 +2532,7 @@ export class Markdown implements Component {
|
|
|
2161
2532
|
: renderTextWithSwatches(
|
|
2162
2533
|
normalizeHtmlEntitiesForTerminal(deltaTabs),
|
|
2163
2534
|
applyText,
|
|
2164
|
-
this.#
|
|
2535
|
+
this.#symbols.colorSwatch || DEFAULT_COLOR_SWATCH_GLYPH,
|
|
2165
2536
|
));
|
|
2166
2537
|
const wrapped = wrapTextWithAnsi(grown, contentWidth);
|
|
2167
2538
|
const fastPaddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
@@ -2214,8 +2585,14 @@ export class Markdown implements Component {
|
|
|
2214
2585
|
// removed): the guard-scan memo's checked region is no longer
|
|
2215
2586
|
// byte-identical, and a cached false verdict may have been based
|
|
2216
2587
|
// on the very CR/ref-def line that was deleted. Invalidate so the
|
|
2217
|
-
// next #lexTokens re-derives on the repaired buffer.
|
|
2588
|
+
// next #lexTokens re-derives on the repaired buffer. The text past
|
|
2589
|
+
// the frozen prefix is no append of what was frozen against either
|
|
2590
|
+
// (a fence line right after the prefix leaves the tail opening on
|
|
2591
|
+
// blank lines a one-pass lex joins to the ones above), so drop it,
|
|
2592
|
+
// and any prefix a rewind set aside.
|
|
2218
2593
|
this.#lastScanValid = false;
|
|
2594
|
+
this.#dropStreamPrefix();
|
|
2595
|
+
this.#streamRewound = undefined;
|
|
2219
2596
|
}
|
|
2220
2597
|
|
|
2221
2598
|
// L2: module-level LRU — survives component disposal/recreation across
|
|
@@ -2327,6 +2704,7 @@ export class Markdown implements Component {
|
|
|
2327
2704
|
textSizing: TERMINAL.textSizing,
|
|
2328
2705
|
bgColorProbe,
|
|
2329
2706
|
headingProbe,
|
|
2707
|
+
symbolsProbe: this.#symbolsProbe,
|
|
2330
2708
|
};
|
|
2331
2709
|
}
|
|
2332
2710
|
// All-primitive signature — compare via the canonical render-cache encoding.
|
|
@@ -2335,7 +2713,7 @@ export class Markdown implements Component {
|
|
|
2335
2713
|
}
|
|
2336
2714
|
|
|
2337
2715
|
#renderCacheKey(normalizedText: string, signature: RenderSignature): string {
|
|
2338
|
-
return `${normalizedText}\x00${signature.width}\x00${signature.paddingX}\x00${signature.paddingY}\x00${signature.codeBlockIndent}\x00${signature.themeId}\x00${signature.defaultTextStyleId}\x00${signature.imageProtocol}\x00${signature.hyperlinks ? 1 : 0}\x00${signature.textSizing ? 1 : 0}\x00${signature.bgColorProbe}\x00${signature.headingProbe}`;
|
|
2716
|
+
return `${normalizedText}\x00${signature.width}\x00${signature.paddingX}\x00${signature.paddingY}\x00${signature.codeBlockIndent}\x00${signature.themeId}\x00${signature.defaultTextStyleId}\x00${signature.imageProtocol}\x00${signature.hyperlinks ? 1 : 0}\x00${signature.textSizing ? 1 : 0}\x00${signature.bgColorProbe}\x00${signature.headingProbe}\x00${signature.symbolsProbe}`;
|
|
2339
2717
|
}
|
|
2340
2718
|
|
|
2341
2719
|
#renderStreamingContentLines(
|
|
@@ -2344,6 +2722,7 @@ export class Markdown implements Component {
|
|
|
2344
2722
|
signature: RenderSignature,
|
|
2345
2723
|
contentWidth: number,
|
|
2346
2724
|
): string[] {
|
|
2725
|
+
if (tokens[tokens.length - 1]?.type !== "list") this.#listItemRowsCache = undefined;
|
|
2347
2726
|
const stableText = this.#streamPrefixText;
|
|
2348
2727
|
const stableTokenCount = this.#streamPrefixTokens?.length ?? 0;
|
|
2349
2728
|
if (stableText === undefined || stableTokenCount === 0 || !normalizedText.startsWith(stableText)) {
|
|
@@ -2356,10 +2735,15 @@ export class Markdown implements Component {
|
|
|
2356
2735
|
// of prefix + tail, so no array handed to a caller is ever mutated.
|
|
2357
2736
|
const reusablePrefix = this.#matchingStreamPrefixLineCache(normalizedText, stableText, signature);
|
|
2358
2737
|
let prefixLines: string[] = [];
|
|
2738
|
+
let marks: PrefixMark[] = [];
|
|
2359
2739
|
let renderedUntil = 0;
|
|
2360
2740
|
if (reusablePrefix && reusablePrefix.tokenCount <= stableTokenCount) {
|
|
2361
2741
|
prefixLines = reusablePrefix.lines;
|
|
2742
|
+
marks = reusablePrefix.marks;
|
|
2362
2743
|
renderedUntil = reusablePrefix.tokenCount;
|
|
2744
|
+
if (renderedUntil < stableTokenCount) {
|
|
2745
|
+
marks.push({ textEnd: reusablePrefix.text.length, tokenCount: renderedUntil, lineEnd: prefixLines.length });
|
|
2746
|
+
}
|
|
2363
2747
|
}
|
|
2364
2748
|
|
|
2365
2749
|
if (renderedUntil < stableTokenCount) {
|
|
@@ -2382,6 +2766,7 @@ export class Markdown implements Component {
|
|
|
2382
2766
|
text: stableText,
|
|
2383
2767
|
tokenCount: stableTokenCount,
|
|
2384
2768
|
lines: prefixLines,
|
|
2769
|
+
marks,
|
|
2385
2770
|
};
|
|
2386
2771
|
|
|
2387
2772
|
if (renderedUntil >= tokens.length) return prefixLines.slice();
|
|
@@ -2431,11 +2816,11 @@ export class Markdown implements Component {
|
|
|
2431
2816
|
}
|
|
2432
2817
|
|
|
2433
2818
|
const recorder: TailRenderRecorder = {
|
|
2434
|
-
//
|
|
2819
|
+
// oxlint-disable-next-line unicorn/no-new-array -- render-cache length preallocation
|
|
2435
2820
|
rows: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2436
|
-
//
|
|
2821
|
+
// oxlint-disable-next-line unicorn/no-new-array -- render-cache length preallocation
|
|
2437
2822
|
raws: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2438
|
-
//
|
|
2823
|
+
// oxlint-disable-next-line unicorn/no-new-array -- render-cache length preallocation
|
|
2439
2824
|
nextTypes: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2440
2825
|
};
|
|
2441
2826
|
const fresh = this.#renderContentLines(tokens, spliceEnd, tokens.length, contentWidth, signature, recorder);
|
|
@@ -2448,11 +2833,11 @@ export class Markdown implements Component {
|
|
|
2448
2833
|
// `start`), so a mostly-frozen document allocates only for the
|
|
2449
2834
|
// unfrozen tail instead of the whole token list every frame.
|
|
2450
2835
|
const tailCount = tokens.length - start;
|
|
2451
|
-
//
|
|
2836
|
+
// oxlint-disable-next-line unicorn/no-new-array -- render-cache length preallocation
|
|
2452
2837
|
const rows: (readonly string[] | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2453
|
-
//
|
|
2838
|
+
// oxlint-disable-next-line unicorn/no-new-array -- render-cache length preallocation
|
|
2454
2839
|
const raws: (string | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2455
|
-
//
|
|
2840
|
+
// oxlint-disable-next-line unicorn/no-new-array -- render-cache length preallocation
|
|
2456
2841
|
const nextTypes: (string | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2457
2842
|
if (cache !== undefined && cache.tokenStart === start) {
|
|
2458
2843
|
for (let i = start; i < Math.min(cache.cachedThrough, spliceEnd); i++) {
|
|
@@ -2914,6 +3299,37 @@ export class Markdown implements Component {
|
|
|
2914
3299
|
return {
|
|
2915
3300
|
applyText: (text: string) => this.#applyDefaultStyle(text),
|
|
2916
3301
|
stylePrefix: this.#getDefaultStylePrefix(),
|
|
3302
|
+
isDefault: true,
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/**
|
|
3307
|
+
* Style context for semantically-themed spans (headings, links) under a
|
|
3308
|
+
* `proseFg` default style: plain runs keep the prose foreground, but the
|
|
3309
|
+
* span's own `md*` theme color must stay the innermost (winning) SGR, so
|
|
3310
|
+
* `applyText` keeps decorations (bold/italic/…) while dropping the
|
|
3311
|
+
* prose color. Non-`proseFg` styles and caller-supplied contexts pass
|
|
3312
|
+
* through unchanged.
|
|
3313
|
+
*/
|
|
3314
|
+
#proseFgExemptContext(styleContext: InlineStyleContext | undefined): InlineStyleContext | undefined {
|
|
3315
|
+
// A caller-supplied context (blockquote identity styling) already governs
|
|
3316
|
+
// this subtree and wins; exemption applies only to the component's own
|
|
3317
|
+
// default style (undefined resolves to it downstream), and stays applied
|
|
3318
|
+
// through nested recursion.
|
|
3319
|
+
if (styleContext && !styleContext.isDefault) return styleContext;
|
|
3320
|
+
if (!this.#defaultTextStyle?.color || !this.#defaultTextStyle?.proseFg) return styleContext;
|
|
3321
|
+
const applyExceptColor = (text: string): string => {
|
|
3322
|
+
let styled = text;
|
|
3323
|
+
if (this.#defaultTextStyle?.bold) styled = this.#theme.bold(styled);
|
|
3324
|
+
if (this.#defaultTextStyle?.italic) styled = this.#theme.italic(styled);
|
|
3325
|
+
if (this.#defaultTextStyle?.strikethrough) styled = this.#theme.strikethrough(styled);
|
|
3326
|
+
if (this.#defaultTextStyle?.underline) styled = this.#theme.underline(styled);
|
|
3327
|
+
return styled;
|
|
3328
|
+
};
|
|
3329
|
+
return {
|
|
3330
|
+
applyText: applyExceptColor,
|
|
3331
|
+
stylePrefix: this.#getStylePrefix(applyExceptColor),
|
|
3332
|
+
isDefault: true,
|
|
2917
3333
|
};
|
|
2918
3334
|
}
|
|
2919
3335
|
|
|
@@ -2945,7 +3361,7 @@ export class Markdown implements Component {
|
|
|
2945
3361
|
case "heading": {
|
|
2946
3362
|
const headingLevel = token.depth;
|
|
2947
3363
|
const headingPrefix = `${"#".repeat(headingLevel)} `;
|
|
2948
|
-
const headingText = this.#renderInlineTokens(token.tokens || [], styleContext);
|
|
3364
|
+
const headingText = this.#renderInlineTokens(token.tokens || [], this.#proseFgExemptContext(styleContext));
|
|
2949
3365
|
const headingPlainText = plainInlineTokens(token.tokens || []);
|
|
2950
3366
|
let styledHeading: string;
|
|
2951
3367
|
if (headingLevel === 1 && TERMINAL.textSizing) {
|
|
@@ -3108,7 +3524,7 @@ export class Markdown implements Component {
|
|
|
3108
3524
|
|
|
3109
3525
|
/** Render a horizontal rule line themed to `width`, matching `sourceChar` when given. */
|
|
3110
3526
|
#renderHrLine(width: number, sourceChar = ""): string {
|
|
3111
|
-
const fillChar = getHrChar(sourceChar, this.#
|
|
3527
|
+
const fillChar = getHrChar(sourceChar, this.#symbols.hrChar);
|
|
3112
3528
|
return this.#theme.hr(fillChar.repeat(Math.min(width, 80)));
|
|
3113
3529
|
}
|
|
3114
3530
|
|
|
@@ -3140,7 +3556,7 @@ export class Markdown implements Component {
|
|
|
3140
3556
|
} else {
|
|
3141
3557
|
const styledLine = applyQuoteStyle(quoteLine.text);
|
|
3142
3558
|
for (const wrappedLine of wrapTextWithAnsi(styledLine, quoteContentWidth)) {
|
|
3143
|
-
lines.push(renderedLine(this.#theme.quoteBorder(`${this.#
|
|
3559
|
+
lines.push(renderedLine(this.#theme.quoteBorder(`${this.#symbols.quoteBorder} `) + wrappedLine));
|
|
3144
3560
|
}
|
|
3145
3561
|
}
|
|
3146
3562
|
}
|
|
@@ -3201,7 +3617,10 @@ export class Markdown implements Component {
|
|
|
3201
3617
|
const segments: string[] = text.split("\n");
|
|
3202
3618
|
return segments.map((segment: string) => (segment === "" ? "" : applyText(segment))).join("\n");
|
|
3203
3619
|
};
|
|
3204
|
-
const swatchGlyph = this.#
|
|
3620
|
+
const swatchGlyph = this.#symbols.colorSwatch || DEFAULT_COLOR_SWATCH_GLYPH;
|
|
3621
|
+
// Set by a line break: the next line's own leading whitespace is dropped.
|
|
3622
|
+
// Every token consumes it, so the space after a styled span that starts
|
|
3623
|
+
// the line stays; a token that renders nothing passes it on.
|
|
3205
3624
|
let trimLeadingWhitespace = false;
|
|
3206
3625
|
const htmlState = createHtmlNormalizationState();
|
|
3207
3626
|
const markHtmlItemWhenContent = (text: string): void => {
|
|
@@ -3209,6 +3628,8 @@ export class Markdown implements Component {
|
|
|
3209
3628
|
};
|
|
3210
3629
|
|
|
3211
3630
|
for (const token of collapseInlineHtml(tokens)) {
|
|
3631
|
+
const lineStart: boolean = trimLeadingWhitespace;
|
|
3632
|
+
trimLeadingWhitespace = false;
|
|
3212
3633
|
if (isMathToken(token)) {
|
|
3213
3634
|
markHtmlItemWhenContent(token.text);
|
|
3214
3635
|
result += applyTextWithNewlines(renderMathToken(token.text));
|
|
@@ -3216,9 +3637,8 @@ export class Markdown implements Component {
|
|
|
3216
3637
|
}
|
|
3217
3638
|
switch (token.type) {
|
|
3218
3639
|
case "text": {
|
|
3219
|
-
const rawText =
|
|
3640
|
+
const rawText = lineStart ? token.text.replace(/^\s+/, "") : token.text;
|
|
3220
3641
|
const text = normalizeHtmlEntitiesForTerminal(rawText);
|
|
3221
|
-
trimLeadingWhitespace = false;
|
|
3222
3642
|
markHtmlItemWhenContent(text);
|
|
3223
3643
|
if (token.tokens) markHtmlItemWhenContent(plainInlineTokens(token.tokens));
|
|
3224
3644
|
// Text tokens in list items can have nested tokens for inline formatting
|
|
@@ -3259,7 +3679,10 @@ export class Markdown implements Component {
|
|
|
3259
3679
|
|
|
3260
3680
|
case "link": {
|
|
3261
3681
|
markHtmlItemWhenContent(token.text);
|
|
3262
|
-
const linkText = this.#renderInlineTokens(
|
|
3682
|
+
const linkText = this.#renderInlineTokens(
|
|
3683
|
+
token.tokens || [],
|
|
3684
|
+
this.#proseFgExemptContext(resolvedStyleContext),
|
|
3685
|
+
);
|
|
3263
3686
|
const styledLinkText = this.#theme.link(this.#theme.underline(linkText));
|
|
3264
3687
|
const href = typeof token.href === "string" ? token.href : "";
|
|
3265
3688
|
const target = (href && this.#theme.resolveLink?.(href)) || href;
|
|
@@ -3299,8 +3722,8 @@ export class Markdown implements Component {
|
|
|
3299
3722
|
result += applyTextWithNewlines(cleaned);
|
|
3300
3723
|
if (cleaned.endsWith("\n")) {
|
|
3301
3724
|
trimLeadingWhitespace = true;
|
|
3302
|
-
} else if (cleaned.length
|
|
3303
|
-
trimLeadingWhitespace =
|
|
3725
|
+
} else if (cleaned.length === 0) {
|
|
3726
|
+
trimLeadingWhitespace = lineStart;
|
|
3304
3727
|
}
|
|
3305
3728
|
}
|
|
3306
3729
|
break;
|
|
@@ -3308,11 +3731,12 @@ export class Markdown implements Component {
|
|
|
3308
3731
|
default:
|
|
3309
3732
|
// Handle any other inline token types as plain text
|
|
3310
3733
|
if ("text" in token && typeof token.text === "string") {
|
|
3311
|
-
const rawText =
|
|
3734
|
+
const rawText = lineStart ? token.text.replace(/^\s+/, "") : token.text;
|
|
3312
3735
|
const text = normalizeHtmlEntitiesForTerminal(rawText);
|
|
3313
|
-
trimLeadingWhitespace = false;
|
|
3314
3736
|
markHtmlItemWhenContent(text);
|
|
3315
3737
|
result += applyTextWithNewlines(text);
|
|
3738
|
+
} else {
|
|
3739
|
+
trimLeadingWhitespace = lineStart;
|
|
3316
3740
|
}
|
|
3317
3741
|
}
|
|
3318
3742
|
}
|
|
@@ -3336,6 +3760,41 @@ export class Markdown implements Component {
|
|
|
3336
3760
|
const indent = " ".repeat(depth);
|
|
3337
3761
|
// Use the list's start property (defaults to 1 for ordered lists)
|
|
3338
3762
|
const startNumber = token.start ?? 1;
|
|
3763
|
+
const signature =
|
|
3764
|
+
depth === 0 &&
|
|
3765
|
+
styleContext === undefined &&
|
|
3766
|
+
this.#defaultTextStyle === undefined &&
|
|
3767
|
+
this.#transientRenderCache &&
|
|
3768
|
+
!this.#renderingStablePrefix &&
|
|
3769
|
+
this.#lastScanCanStream &&
|
|
3770
|
+
canCacheMarkdownListItems(this.#theme)
|
|
3771
|
+
? this.#activeRenderSignature
|
|
3772
|
+
: undefined;
|
|
3773
|
+
const themeEpoch = signature === undefined ? undefined : getThemeEpoch();
|
|
3774
|
+
const previous = this.#listItemRowsCache;
|
|
3775
|
+
const reusable =
|
|
3776
|
+
signature !== undefined &&
|
|
3777
|
+
themeEpoch !== undefined &&
|
|
3778
|
+
previous !== undefined &&
|
|
3779
|
+
previous.width === width &&
|
|
3780
|
+
previous.themeEpoch === themeEpoch &&
|
|
3781
|
+
previous.ordered === token.ordered &&
|
|
3782
|
+
previous.start === token.start &&
|
|
3783
|
+
previous.loose === token.loose &&
|
|
3784
|
+
this.#signatureEquals(previous.signature, signature)
|
|
3785
|
+
? previous
|
|
3786
|
+
: undefined;
|
|
3787
|
+
let reusableCount = 0;
|
|
3788
|
+
if (reusable !== undefined) {
|
|
3789
|
+
while (
|
|
3790
|
+
reusableCount < token.items.length &&
|
|
3791
|
+
token.items[reusableCount]?.raw === reusable.raws[reusableCount]
|
|
3792
|
+
) {
|
|
3793
|
+
reusableCount++;
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
const raws: string[] | undefined = signature === undefined ? undefined : [];
|
|
3797
|
+
const rows: RenderedLine[][] | undefined = signature === undefined ? undefined : [];
|
|
3339
3798
|
const pushWrapped = (line: RenderedLine, firstPrefix: string, continuationPrefix: string): void => {
|
|
3340
3799
|
if (line.literalCode) {
|
|
3341
3800
|
const wrappedLiteralRows = wrapTextWithAnsi(line.text, Math.max(1, width));
|
|
@@ -3371,6 +3830,14 @@ export class Markdown implements Component {
|
|
|
3371
3830
|
|
|
3372
3831
|
for (let i = 0; i < token.items.length; i++) {
|
|
3373
3832
|
const item = token.items[i];
|
|
3833
|
+
if (i < reusableCount && reusable !== undefined && raws !== undefined && rows !== undefined) {
|
|
3834
|
+
const cached = reusable.rows[i]!;
|
|
3835
|
+
for (const row of cached) lines.push(row);
|
|
3836
|
+
raws.push(item.raw);
|
|
3837
|
+
rows.push(cached);
|
|
3838
|
+
continue;
|
|
3839
|
+
}
|
|
3840
|
+
const rowStart = lines.length;
|
|
3374
3841
|
const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
|
|
3375
3842
|
const firstPrefix = indent + this.#theme.listBullet(bullet);
|
|
3376
3843
|
// Continuation rows align under the item text, so the hang matches the
|
|
@@ -3400,6 +3867,22 @@ export class Markdown implements Component {
|
|
|
3400
3867
|
} else {
|
|
3401
3868
|
lines.push(renderedLine(firstPrefix));
|
|
3402
3869
|
}
|
|
3870
|
+
if (raws !== undefined && rows !== undefined) {
|
|
3871
|
+
raws.push(item.raw);
|
|
3872
|
+
rows.push(lines.slice(rowStart));
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
if (signature !== undefined && themeEpoch !== undefined && raws !== undefined && rows !== undefined) {
|
|
3876
|
+
this.#listItemRowsCache = {
|
|
3877
|
+
signature,
|
|
3878
|
+
width,
|
|
3879
|
+
themeEpoch,
|
|
3880
|
+
ordered: token.ordered,
|
|
3881
|
+
start: token.start,
|
|
3882
|
+
loose: token.loose,
|
|
3883
|
+
raws,
|
|
3884
|
+
rows,
|
|
3885
|
+
};
|
|
3403
3886
|
}
|
|
3404
3887
|
|
|
3405
3888
|
return lines;
|
|
@@ -3575,7 +4058,7 @@ export class Markdown implements Component {
|
|
|
3575
4058
|
let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
3576
4059
|
|
|
3577
4060
|
if (minCellsWidth > availableForCells) {
|
|
3578
|
-
//
|
|
4061
|
+
// oxlint-disable-next-line unicorn/no-new-array -- column-width allocation
|
|
3579
4062
|
minColumnWidths = new Array(numCols).fill(1);
|
|
3580
4063
|
const remaining = availableForCells - numCols;
|
|
3581
4064
|
|
|
@@ -3642,7 +4125,7 @@ export class Markdown implements Component {
|
|
|
3642
4125
|
}
|
|
3643
4126
|
}
|
|
3644
4127
|
|
|
3645
|
-
const t = this.#
|
|
4128
|
+
const t = this.#symbols.table;
|
|
3646
4129
|
const h = t.horizontal;
|
|
3647
4130
|
const v = t.vertical;
|
|
3648
4131
|
|