mixdog 0.8.1 → 0.9.1
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/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* markdown/format-token.mjs — token → ANSI string renderer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* + helpers), adapted for this CLI:
|
|
4
|
+
* Token → ANSI string renderer:
|
|
6
5
|
* - chalk is forced to truecolor (level 3) so colors render regardless of the
|
|
7
6
|
* ambient TTY detection (we control the surface).
|
|
8
7
|
* - The `permission`/code accent and blockquote bar come from our theme.mjs.
|
|
9
|
-
* - `code` (fenced)
|
|
10
|
-
*
|
|
8
|
+
* - `code` (fenced) uses cli-highlight + theme syntax palette; `codespan`
|
|
9
|
+
* (inline) gets the accent color.
|
|
11
10
|
* - `table` is NOT handled here — the React component (MarkdownTable.jsx)
|
|
12
|
-
* renders tables with proper ink Box layout
|
|
11
|
+
* renders tables with proper ink Box layout (hybrid split).
|
|
13
12
|
* - Hyperlinks/issue-ref linkify are dropped (no OSC-8 dependency); link text
|
|
14
13
|
* is shown plainly with its URL.
|
|
15
14
|
*/
|
|
16
15
|
import { Chalk } from 'chalk';
|
|
16
|
+
import { highlight, supportsLanguage } from 'cli-highlight';
|
|
17
17
|
import stripAnsi from 'strip-ansi';
|
|
18
|
-
import
|
|
19
|
-
import
|
|
18
|
+
import stringWidth from 'string-width';
|
|
19
|
+
import wrapAnsi from 'wrap-ansi';
|
|
20
|
+
import { theme, getThemeVersion } from '../theme.mjs';
|
|
21
|
+
import { BLOCKQUOTE_BAR, HR_LINE } from '../figures.mjs';
|
|
20
22
|
|
|
21
23
|
// Force truecolor so chalk emits 24-bit SGR even when the ambient level is 0.
|
|
22
24
|
// ink's <Text> passes these escapes through verbatim.
|
|
23
25
|
const chalk = new Chalk({ level: 3 });
|
|
24
26
|
|
|
25
|
-
// Use \n unconditionally (
|
|
27
|
+
// Use \n unconditionally (os.EOL's \r breaks segment mapping).
|
|
26
28
|
const EOL = '\n';
|
|
27
29
|
|
|
28
30
|
/** Parse an `rgb(r,g,b)` theme string into a chalk colorizer. */
|
|
@@ -32,8 +34,78 @@ function rgbColor(str) {
|
|
|
32
34
|
return chalk.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Parse an `rgb(r,g,b)` theme string into a TRUECOLOR BACKGROUND wrapper. Emits
|
|
39
|
+
* `48;2;R;G;B` … `49` (bg reset) around the string so AnsiText (case 48) maps it
|
|
40
|
+
* to an ink backgroundColor, giving a code line a tinted band. Returns identity
|
|
41
|
+
* when the string is not a valid rgb() so a missing key never corrupts output.
|
|
42
|
+
*/
|
|
43
|
+
function rgbBg(str) {
|
|
44
|
+
const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(str || ''));
|
|
45
|
+
if (!m) return (s) => s;
|
|
46
|
+
const open = `\x1b[48;2;${+m[1]};${+m[2]};${+m[3]}m`;
|
|
47
|
+
return (s) => `${open}${s}\x1b[49m`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Colorizers are derived from the active theme's md* keys. They are cached and
|
|
51
|
+
// rebuilt only when the theme version changes, so a `/theme` switch takes
|
|
52
|
+
// effect on the next render without recomputing a chalk fn per token.
|
|
53
|
+
let _colorizerVersion = -1;
|
|
54
|
+
let _colorizers = null;
|
|
55
|
+
function colorizers() {
|
|
56
|
+
const version = getThemeVersion();
|
|
57
|
+
if (_colorizers && _colorizerVersion === version) return _colorizers;
|
|
58
|
+
_colorizerVersion = version;
|
|
59
|
+
_colorizers = {
|
|
60
|
+
accent: rgbColor(theme.mdCode), // inline codespan
|
|
61
|
+
codeBlock: rgbColor(theme.mdCodeBlock), // fenced code block body
|
|
62
|
+
headingAccent: rgbColor(theme.mdHeading),
|
|
63
|
+
quoteBorder: rgbColor(theme.mdQuoteBorder),
|
|
64
|
+
quoteText: rgbColor(theme.mdQuote),
|
|
65
|
+
hrLine: rgbColor(theme.mdHr),
|
|
66
|
+
listBullet: rgbColor(theme.mdListBullet),
|
|
67
|
+
};
|
|
68
|
+
return _colorizers;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extended colorizers (fence border, link, emphasis, diff + syntax classes).
|
|
72
|
+
// Cached on the same theme-version key so a /theme switch rebuilds both maps.
|
|
73
|
+
let _extraVersion = -1;
|
|
74
|
+
let _extra = null;
|
|
75
|
+
export function extraColorizers() {
|
|
76
|
+
const version = getThemeVersion();
|
|
77
|
+
if (_extra && _extraVersion === version) return _extra;
|
|
78
|
+
_extraVersion = version;
|
|
79
|
+
const fallbackBody = rgbColor(theme.mdCodeBlock);
|
|
80
|
+
_extra = {
|
|
81
|
+
fenceBorder: rgbColor(theme.mdCodeBlockBorder ?? theme.mdHr),
|
|
82
|
+
link: rgbColor(theme.mdLink ?? theme.code),
|
|
83
|
+
linkText: rgbColor(theme.mdLinkText ?? theme.mdCode),
|
|
84
|
+
strong: rgbColor(theme.mdStrong ?? theme.mdHeading),
|
|
85
|
+
emph: rgbColor(theme.mdEmph ?? theme.mdHeading),
|
|
86
|
+
// diff/patch
|
|
87
|
+
diffAdded: rgbColor(theme.mdDiffAdded ?? theme.success),
|
|
88
|
+
diffRemoved: rgbColor(theme.mdDiffRemoved ?? theme.error),
|
|
89
|
+
diffHunk: rgbColor(theme.mdDiffHunk ?? theme.code),
|
|
90
|
+
diffHeader: rgbColor(theme.mdDiffHeader ?? theme.mdHeading),
|
|
91
|
+
diffContext: rgbColor(theme.mdDiffContext ?? theme.subtle),
|
|
92
|
+
// syntax classes (fall back to plain code-block body when key missing)
|
|
93
|
+
synComment: rgbColor(theme.syntaxComment ?? theme.subtle),
|
|
94
|
+
synKeyword: rgbColor(theme.syntaxKeyword ?? theme.mdCodeBlock),
|
|
95
|
+
synFunction: rgbColor(theme.syntaxFunction ?? theme.mdCodeBlock),
|
|
96
|
+
synVariable: rgbColor(theme.syntaxVariable ?? theme.mdCodeBlock),
|
|
97
|
+
synString: rgbColor(theme.syntaxString ?? theme.mdCodeBlock),
|
|
98
|
+
synNumber: rgbColor(theme.syntaxNumber ?? theme.mdCodeBlock),
|
|
99
|
+
synType: rgbColor(theme.syntaxType ?? theme.mdCodeBlock),
|
|
100
|
+
synOperator: rgbColor(theme.syntaxOperator ?? theme.mdCodeBlock),
|
|
101
|
+
synPunct: rgbColor(theme.syntaxPunctuation ?? theme.mdCodeBlock),
|
|
102
|
+
body: fallbackBody,
|
|
103
|
+
// Truecolor background band for fenced code blocks (per-line wrap).
|
|
104
|
+
codeBg: rgbBg(theme.mdCodeBlockBg ?? theme.background),
|
|
105
|
+
codeSpanBg: rgbBg(theme.mdCodeSpanBg ?? theme.mdCodeBlockBg ?? theme.background),
|
|
106
|
+
};
|
|
107
|
+
return _extra;
|
|
108
|
+
}
|
|
37
109
|
|
|
38
110
|
// marked 14 HTML-encodes token.text / codespan.text (`"` → `"`, `&` →
|
|
39
111
|
// `&`, `<`→`<`, `>`→`>`, `'`→`'`), but we render to a terminal,
|
|
@@ -53,80 +125,575 @@ function decodeEntities(s) {
|
|
|
53
125
|
.replace(/&/g, '&');
|
|
54
126
|
}
|
|
55
127
|
|
|
128
|
+
// ── Fenced code block rendering ─────────────────────────────────────────────
|
|
129
|
+
// Flush-left wrapped body with per-line background band (no ``` fences / lang row).
|
|
130
|
+
// Diff/patch languages (and bodies that look like unified diffs) keep the diff
|
|
131
|
+
// highlighter; other languages use cli-highlight (highlight.js) themed from our
|
|
132
|
+
// syntax* palette; unknown languages fall back to flat `mdCodeBlock` body color.
|
|
133
|
+
const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
|
|
134
|
+
|
|
135
|
+
/** Wrap text to width, ANSI-aware (lockstep with table-layout hard wrap). */
|
|
136
|
+
function wrapTextToWidth(text, width, options) {
|
|
137
|
+
if (width <= 0) return [text];
|
|
138
|
+
const trimmedText = String(text).trimEnd();
|
|
139
|
+
const wrapped = wrapAnsi(trimmedText, width, {
|
|
140
|
+
hard: options?.hard ?? false,
|
|
141
|
+
trim: false,
|
|
142
|
+
wordWrap: true,
|
|
143
|
+
});
|
|
144
|
+
const lines = wrapped.split('\n').filter((line) => line.length > 0);
|
|
145
|
+
return lines.length > 0 ? lines : [''];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Hard-wrap so every line satisfies stringWidth(line) <= width. */
|
|
149
|
+
function hardWrapAnsiLines(text, width) {
|
|
150
|
+
const max = Math.max(1, Math.floor(Number(width) || 1));
|
|
151
|
+
const input = String(text ?? '');
|
|
152
|
+
if (!input) return [''];
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const softLine of wrapTextToWidth(input, max, { hard: true })) {
|
|
155
|
+
let rest = softLine;
|
|
156
|
+
while (rest.length > 0 && stringWidth(rest) > max) {
|
|
157
|
+
let take = 1;
|
|
158
|
+
for (let i = 1; i <= rest.length; i++) {
|
|
159
|
+
if (stringWidth(rest.slice(0, i)) <= max) take = i;
|
|
160
|
+
else break;
|
|
161
|
+
}
|
|
162
|
+
out.push(rest.slice(0, take));
|
|
163
|
+
rest = rest.slice(take);
|
|
164
|
+
}
|
|
165
|
+
if (rest.length > 0) out.push(rest);
|
|
166
|
+
}
|
|
167
|
+
return out.length > 0 ? out : [''];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Wrap one logical code line (ANSI content) to max visible width. */
|
|
171
|
+
function wrapCodeLine(ansiContent, maxLineWidth) {
|
|
172
|
+
const contentMax = Math.max(1, maxLineWidth);
|
|
173
|
+
return hardWrapAnsiLines(ansiContent, contentMax);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Visible terminal width of an ANSI-colored line (codes excluded). */
|
|
177
|
+
function visibleLineWidth(line) {
|
|
178
|
+
return stringWidth(stripAnsi(String(line ?? '')));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Max visible width across lang label + body lines (content-based, not terminal). */
|
|
182
|
+
function codeBlockContentWidth(langLine, bodyLines) {
|
|
183
|
+
const all = [...(langLine ? [langLine] : []), ...bodyLines];
|
|
184
|
+
if (all.length === 0) return 0;
|
|
185
|
+
return Math.max(1, ...all.map(visibleLineWidth));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Apply code-block bg to one line, padded to a shared compact block width. */
|
|
189
|
+
function tintCodeLineToBlockWidth(line, codeBg, blockWidth) {
|
|
190
|
+
const w = visibleLineWidth(line);
|
|
191
|
+
const pad = Math.max(0, blockWidth - w);
|
|
192
|
+
return codeBg(`${line ?? ''}${' '.repeat(pad)}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function tintCodeBlockBody(langLine, bodyLines, codeBg) {
|
|
196
|
+
const blockWidth = codeBlockContentWidth(langLine, bodyLines);
|
|
197
|
+
const langOut = langLine ? tintCodeLineToBlockWidth(langLine, codeBg, blockWidth) : '';
|
|
198
|
+
const bodyOut = bodyLines.map((line) => tintCodeLineToBlockWidth(line, codeBg, blockWidth)).join(EOL);
|
|
199
|
+
return { langOut, bodyOut };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
|
|
203
|
+
function contentWidthAfterPrefix(width, prefix) {
|
|
204
|
+
const base = Number(width) || 0;
|
|
205
|
+
if (base <= 0) return 0;
|
|
206
|
+
return Math.max(8, base - stringWidth(String(prefix ?? '')));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Normalize a fenced info-string to a bare lowercase language token. */
|
|
210
|
+
function normalizeLang(lang) {
|
|
211
|
+
return String(lang || '')
|
|
212
|
+
.trim()
|
|
213
|
+
.split(/\s+/)[0]
|
|
214
|
+
.toLowerCase();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Heuristic: does this code body look like a unified diff/patch? Requires at
|
|
219
|
+
* least one +/- body line AND a hunk header or file header, so ordinary code
|
|
220
|
+
* with leading +/- (rare) is not misclassified.
|
|
221
|
+
*/
|
|
222
|
+
export function looksLikeUnifiedDiff(text) {
|
|
223
|
+
const lines = String(text ?? '').split(EOL);
|
|
224
|
+
let hasHunk = false;
|
|
225
|
+
let hasFileHeader = false;
|
|
226
|
+
let hasSign = false;
|
|
227
|
+
let hasStat = false;
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
if (/^@@ .* @@/.test(line) || /^@@ /.test(line)) hasHunk = true;
|
|
230
|
+
if (/^(\+\+\+ |--- |diff --git |index [0-9a-f]+)/.test(line)) hasFileHeader = true;
|
|
231
|
+
if (/^[+-](?![+-])/.test(line)) hasSign = true;
|
|
232
|
+
// `git diff --stat` summary rows: `path | 4 +-` and the trailer
|
|
233
|
+
// `N files changed, M insertions(+), K deletions(-)`.
|
|
234
|
+
if (DIFF_STAT_FILE_RE.test(line) || DIFF_STAT_SUMMARY_RE.test(line)) hasStat = true;
|
|
235
|
+
}
|
|
236
|
+
return ((hasHunk || hasFileHeader) && hasSign) || hasStat;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// `git diff --stat` rows. FILE: `<path> | <count> <+/-/ graph>` (count may be
|
|
240
|
+
// `Bin`); SUMMARY: `N file(s) changed[, M insertion…][, K deletion…]`.
|
|
241
|
+
const DIFF_STAT_FILE_RE = /^(\s*)(.+?)(\s+\|\s+)(Bin\b.*|\d+\s*[+\-]*)\s*$/;
|
|
242
|
+
const DIFF_STAT_SUMMARY_RE = /^\s*\d+\s+files?\s+changed\b/;
|
|
243
|
+
|
|
244
|
+
/** Color a `git diff --stat` file row: dim path/sep, green `+`, red `-`. */
|
|
245
|
+
function colorizeDiffStatLine(line, c) {
|
|
246
|
+
const m = DIFF_STAT_FILE_RE.exec(line);
|
|
247
|
+
if (m) {
|
|
248
|
+
const [, indent, path, sep, tail] = m;
|
|
249
|
+
// The tail is either `Bin …` or `<count> <graph>` where graph is +/-.
|
|
250
|
+
const countMatch = /^(\d+)(\s*)([+\-]*)\s*$/.exec(tail);
|
|
251
|
+
let coloredTail;
|
|
252
|
+
if (countMatch) {
|
|
253
|
+
const [, count, gap, bars] = countMatch;
|
|
254
|
+
const pluses = c.diffAdded('+'.repeat((bars.match(/\+/g) || []).length));
|
|
255
|
+
const minuses = c.diffRemoved('-'.repeat((bars.match(/-/g) || []).length));
|
|
256
|
+
coloredTail = `${c.diffContext(count)}${gap}${pluses}${minuses}`;
|
|
257
|
+
} else {
|
|
258
|
+
coloredTail = c.diffContext(tail);
|
|
259
|
+
}
|
|
260
|
+
return `${indent}${c.diffContext(path)}${c.diffContext(sep)}${coloredTail}`;
|
|
261
|
+
}
|
|
262
|
+
return c.diffContext(line);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Classify and color a single unified-diff line. */
|
|
266
|
+
export function colorizeDiffLine(line, c) {
|
|
267
|
+
// File/section headers first so +++/--- are never treated as add/remove.
|
|
268
|
+
if (/^(\+\+\+|---)(\s|$)/.test(line)) return c.diffHeader(line);
|
|
269
|
+
if (/^(diff --git |index [0-9a-f]|new file|deleted file|rename |similarity |old mode|new mode)/.test(line)) {
|
|
270
|
+
return c.diffHeader(line);
|
|
271
|
+
}
|
|
272
|
+
if (/^@@/.test(line)) return c.diffHunk(line);
|
|
273
|
+
if (/^\+/.test(line)) return c.diffAdded(line);
|
|
274
|
+
if (/^-/.test(line)) return c.diffRemoved(line);
|
|
275
|
+
if (/^\\ No newline/.test(line)) return c.diffContext(line);
|
|
276
|
+
// `git diff --stat` summary trailer + per-file rows.
|
|
277
|
+
if (DIFF_STAT_SUMMARY_RE.test(line)) return colorizeDiffStatTrailer(line, c);
|
|
278
|
+
if (DIFF_STAT_FILE_RE.test(line)) return colorizeDiffStatLine(line, c);
|
|
279
|
+
return c.diffContext(line);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Color the `N files changed, M insertions(+), K deletions(-)` trailer. */
|
|
283
|
+
function colorizeDiffStatTrailer(line, c) {
|
|
284
|
+
return line.replace(/(\d+)(\s+insertions?\(\+\))/g, (_, n, rest) => `${c.diffAdded(n)}${c.diffContext(rest)}`)
|
|
285
|
+
.replace(/(\d+)(\s+deletions?\(-\))/g, (_, n, rest) => `${c.diffRemoved(n)}${c.diffContext(rest)}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectDiffBodyLines(text, c, bandWidth) {
|
|
289
|
+
const lines = [];
|
|
290
|
+
for (const line of String(text ?? '').split(EOL)) {
|
|
291
|
+
const colored = colorizeDiffLine(line, c);
|
|
292
|
+
lines.push(...wrapCodeLine(colored, bandWidth));
|
|
293
|
+
}
|
|
294
|
+
return lines;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── cli-highlight (highlight.js) ────────────────────────────────────────────
|
|
298
|
+
/** Internal family → highlight.js language id (when alias alone is insufficient). */
|
|
299
|
+
const FAMILY_TO_HLJS = {
|
|
300
|
+
js: 'javascript',
|
|
301
|
+
ts: 'typescript',
|
|
302
|
+
tsx: 'tsx',
|
|
303
|
+
jsx: 'jsx',
|
|
304
|
+
json: 'json',
|
|
305
|
+
sh: 'bash',
|
|
306
|
+
py: 'python',
|
|
307
|
+
css: 'css',
|
|
308
|
+
html: 'xml',
|
|
309
|
+
go: 'go',
|
|
310
|
+
rust: 'rust',
|
|
311
|
+
java: 'java',
|
|
312
|
+
c: 'cpp',
|
|
313
|
+
ruby: 'ruby',
|
|
314
|
+
sql: 'sql',
|
|
315
|
+
yaml: 'yaml',
|
|
316
|
+
toml: 'ini',
|
|
317
|
+
kotlin: 'kotlin',
|
|
318
|
+
swift: 'swift',
|
|
319
|
+
php: 'php',
|
|
320
|
+
csharp: 'csharp',
|
|
321
|
+
dockerfile: 'dockerfile',
|
|
322
|
+
protobuf: 'protobuf',
|
|
323
|
+
scala: 'scala',
|
|
324
|
+
dart: 'dart',
|
|
325
|
+
lua: 'lua',
|
|
326
|
+
perl: 'perl',
|
|
327
|
+
r: 'r',
|
|
328
|
+
objc: 'objectivec',
|
|
329
|
+
powershell: 'powershell',
|
|
330
|
+
makefile: 'makefile',
|
|
331
|
+
nginx: 'nginx',
|
|
332
|
+
ini: 'ini',
|
|
333
|
+
vim: 'vim',
|
|
334
|
+
haskell: 'haskell',
|
|
335
|
+
elixir: 'elixir',
|
|
336
|
+
clojure: 'clojure',
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const LANG_FAMILY = {
|
|
340
|
+
js: 'js', javascript: 'js', mjs: 'js', cjs: 'js',
|
|
341
|
+
ts: 'ts', typescript: 'ts', jsx: 'jsx', tsx: 'tsx',
|
|
342
|
+
json: 'json', json5: 'json',
|
|
343
|
+
bash: 'sh', sh: 'sh', shell: 'sh', zsh: 'sh',
|
|
344
|
+
python: 'py', py: 'py',
|
|
345
|
+
css: 'css', scss: 'css', less: 'css',
|
|
346
|
+
html: 'html', xml: 'html',
|
|
347
|
+
md: 'md', markdown: 'md',
|
|
348
|
+
go: 'go', golang: 'go',
|
|
349
|
+
rust: 'rust', rs: 'rust',
|
|
350
|
+
java: 'java',
|
|
351
|
+
c: 'c', cpp: 'c', 'c++': 'c', cc: 'c', h: 'c', hpp: 'c',
|
|
352
|
+
ruby: 'ruby', rb: 'ruby',
|
|
353
|
+
sql: 'sql',
|
|
354
|
+
yaml: 'yaml', yml: 'yaml',
|
|
355
|
+
toml: 'toml',
|
|
356
|
+
kotlin: 'kotlin', kt: 'kotlin', kts: 'kotlin',
|
|
357
|
+
swift: 'swift',
|
|
358
|
+
php: 'php',
|
|
359
|
+
csharp: 'csharp', cs: 'csharp', 'c#': 'csharp',
|
|
360
|
+
dockerfile: 'dockerfile', docker: 'dockerfile',
|
|
361
|
+
graphql: 'graphql', gql: 'graphql',
|
|
362
|
+
protobuf: 'protobuf', proto: 'protobuf',
|
|
363
|
+
scala: 'scala',
|
|
364
|
+
dart: 'dart',
|
|
365
|
+
lua: 'lua',
|
|
366
|
+
perl: 'perl', pl: 'perl',
|
|
367
|
+
r: 'r', rl: 'r',
|
|
368
|
+
objc: 'objc', 'objective-c': 'objc', 'obj-c': 'objc',
|
|
369
|
+
powershell: 'powershell', ps1: 'powershell', ps: 'powershell', pwsh: 'powershell',
|
|
370
|
+
makefile: 'makefile', make: 'makefile',
|
|
371
|
+
nginx: 'nginx',
|
|
372
|
+
ini: 'ini',
|
|
373
|
+
vim: 'vim',
|
|
374
|
+
haskell: 'haskell', hs: 'haskell',
|
|
375
|
+
elixir: 'elixir', ex: 'elixir', exs: 'elixir',
|
|
376
|
+
clojure: 'clojure', clj: 'clojure',
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const HIGHLIGHT_CACHE_MAX = 300;
|
|
380
|
+
const highlightCache = new Map();
|
|
381
|
+
|
|
382
|
+
/** @internal Test-only introspection for highlight LRU cache. */
|
|
383
|
+
export function _highlightCacheSizeForTests() {
|
|
384
|
+
return highlightCache.size;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let _hljsThemeVersion = -1;
|
|
388
|
+
let _hljsTheme = null;
|
|
389
|
+
|
|
390
|
+
/** Map highlight.js token classes to chalk truecolor fns from the active theme. */
|
|
391
|
+
function buildCliHighlightTheme(c) {
|
|
392
|
+
const plain = (s) => c.body(s);
|
|
393
|
+
return {
|
|
394
|
+
default: plain,
|
|
395
|
+
keyword: c.synKeyword,
|
|
396
|
+
built_in: c.synType,
|
|
397
|
+
type: c.synType,
|
|
398
|
+
literal: c.synKeyword,
|
|
399
|
+
number: c.synNumber,
|
|
400
|
+
regexp: c.synString,
|
|
401
|
+
string: c.synString,
|
|
402
|
+
subst: plain,
|
|
403
|
+
symbol: plain,
|
|
404
|
+
class: c.synType,
|
|
405
|
+
function: c.synFunction,
|
|
406
|
+
title: c.synFunction,
|
|
407
|
+
params: c.synVariable,
|
|
408
|
+
comment: c.synComment,
|
|
409
|
+
doctag: c.synComment,
|
|
410
|
+
meta: c.synComment,
|
|
411
|
+
section: c.synType,
|
|
412
|
+
tag: c.synPunct,
|
|
413
|
+
name: c.synFunction,
|
|
414
|
+
builtin: c.synType,
|
|
415
|
+
attr: c.synType,
|
|
416
|
+
attribute: c.synType,
|
|
417
|
+
variable: c.synVariable,
|
|
418
|
+
selector: c.synKeyword,
|
|
419
|
+
template: c.synVariable,
|
|
420
|
+
bullet: plain,
|
|
421
|
+
code: plain,
|
|
422
|
+
emphasis: plain,
|
|
423
|
+
strong: plain,
|
|
424
|
+
formula: plain,
|
|
425
|
+
link: plain,
|
|
426
|
+
quote: plain,
|
|
427
|
+
addition: c.synString,
|
|
428
|
+
deletion: c.synString,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function getCliHighlightTheme() {
|
|
433
|
+
const version = getThemeVersion();
|
|
434
|
+
if (_hljsTheme && _hljsThemeVersion === version) return _hljsTheme;
|
|
435
|
+
_hljsThemeVersion = version;
|
|
436
|
+
_hljsTheme = buildCliHighlightTheme(extraColorizers());
|
|
437
|
+
return _hljsTheme;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function hljsLanguageFromFamily(family) {
|
|
441
|
+
if (!family || family === 'md') return null;
|
|
442
|
+
const mapped = FAMILY_TO_HLJS[family];
|
|
443
|
+
if (mapped && supportsLanguage(mapped)) return mapped;
|
|
444
|
+
if (supportsLanguage(family)) return family;
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Resolve a fenced info-string to a highlight.js language id. */
|
|
449
|
+
function resolveHljsLanguage(lang) {
|
|
450
|
+
const normalized = normalizeLang(lang);
|
|
451
|
+
if (!normalized) return null;
|
|
452
|
+
const viaFamily = hljsLanguageFromFamily(LANG_FAMILY[normalized]);
|
|
453
|
+
if (viaFamily) return viaFamily;
|
|
454
|
+
return supportsLanguage(normalized) ? normalized : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Highlight source with cli-highlight; returns null when language is unsupported
|
|
459
|
+
* or highlighting fails (caller falls back to flat body color).
|
|
460
|
+
*/
|
|
461
|
+
function highlightCodeText(text, hljsLang) {
|
|
462
|
+
if (!hljsLang || !supportsLanguage(hljsLang)) return null;
|
|
463
|
+
const src = String(text ?? '');
|
|
464
|
+
if (!src.trim()) return null;
|
|
465
|
+
if (!/[A-Za-z0-9]/.test(src)) return null;
|
|
466
|
+
const cacheKey = `${hljsLang}|${getThemeVersion()}|${src}`;
|
|
467
|
+
const cached = highlightCache.get(cacheKey);
|
|
468
|
+
if (cached !== undefined) {
|
|
469
|
+
highlightCache.delete(cacheKey);
|
|
470
|
+
highlightCache.set(cacheKey, cached);
|
|
471
|
+
return cached;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const out = highlight(src, {
|
|
475
|
+
language: hljsLang,
|
|
476
|
+
theme: getCliHighlightTheme(),
|
|
477
|
+
ignoreIllegals: true,
|
|
478
|
+
});
|
|
479
|
+
if (highlightCache.size >= HIGHLIGHT_CACHE_MAX) {
|
|
480
|
+
const first = highlightCache.keys().next().value;
|
|
481
|
+
if (first !== undefined) highlightCache.delete(first);
|
|
482
|
+
}
|
|
483
|
+
highlightCache.set(cacheKey, out);
|
|
484
|
+
return out;
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Highlight a single line (tool expanded output); uses the same theme map. */
|
|
491
|
+
export function highlightCodeLine(line, family, c) {
|
|
492
|
+
const hljsLang = hljsLanguageFromFamily(family);
|
|
493
|
+
const highlighted = highlightCodeText(line, hljsLang);
|
|
494
|
+
if (highlighted != null) return highlighted;
|
|
495
|
+
return line ? c.body(line) : '';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Highlight a multi-line block in one cli-highlight call; returns one ANSI string per line.
|
|
500
|
+
* On miss, each line is flat-colored with `c.body`.
|
|
501
|
+
*/
|
|
502
|
+
export function highlightCodeBlockToLines(text, family, c) {
|
|
503
|
+
const hljsLang = hljsLanguageFromFamily(family);
|
|
504
|
+
const raw = String(text ?? '');
|
|
505
|
+
const highlighted = hljsLang ? highlightCodeText(raw, hljsLang) : null;
|
|
506
|
+
if (highlighted != null) {
|
|
507
|
+
return highlighted.split(EOL);
|
|
508
|
+
}
|
|
509
|
+
return raw.split(EOL).map((line) => (line ? c.body(line) : ''));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function collectFlatBodyLines(text, codeBlock, bandWidth) {
|
|
513
|
+
const lines = [];
|
|
514
|
+
for (const line of String(text ?? '').split(EOL)) {
|
|
515
|
+
const colored = codeBlock(line);
|
|
516
|
+
lines.push(...wrapCodeLine(colored, bandWidth));
|
|
517
|
+
}
|
|
518
|
+
return lines;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function collectHighlightedBodyLines(text, hljsLang, bandWidth) {
|
|
522
|
+
const highlighted = highlightCodeText(text, hljsLang);
|
|
523
|
+
const { codeBlock } = colorizers();
|
|
524
|
+
const source = highlighted != null
|
|
525
|
+
? highlighted
|
|
526
|
+
: String(text ?? '').split(EOL).map(codeBlock).join(EOL);
|
|
527
|
+
const lines = [];
|
|
528
|
+
for (const line of source.split(EOL)) {
|
|
529
|
+
lines.push(...wrapCodeLine(line, bandWidth));
|
|
530
|
+
}
|
|
531
|
+
return lines;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Render a fenced `code` token: flush-left wrapped body with background band
|
|
536
|
+
* and language-aware coloring (no visible ``` fence lines or lang label).
|
|
537
|
+
*/
|
|
538
|
+
function renderCodeBlock(token, width = 0) {
|
|
539
|
+
const { codeBlock } = colorizers();
|
|
540
|
+
const c = extraColorizers();
|
|
541
|
+
const lang = normalizeLang(token.lang);
|
|
542
|
+
const text = decodeEntities(token.text ?? '');
|
|
543
|
+
const bandWidth = Math.max(8, Number(width) || 80);
|
|
544
|
+
|
|
545
|
+
let bodyLines;
|
|
546
|
+
if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
|
|
547
|
+
bodyLines = collectDiffBodyLines(text, c, bandWidth);
|
|
548
|
+
} else {
|
|
549
|
+
const hljsLang = resolveHljsLanguage(lang);
|
|
550
|
+
const family = LANG_FAMILY[lang];
|
|
551
|
+
if (hljsLang && family !== 'md') {
|
|
552
|
+
bodyLines = collectHighlightedBodyLines(text, hljsLang, bandWidth);
|
|
553
|
+
} else {
|
|
554
|
+
bodyLines = collectFlatBodyLines(text, codeBlock, bandWidth);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const { bodyOut } = tintCodeBlockBody(null, bodyLines, c.codeBg);
|
|
558
|
+
return `${bodyOut}${bodyOut ? EOL : ''}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function numberToLetter(n) {
|
|
562
|
+
let result = '';
|
|
563
|
+
while (n > 0) {
|
|
564
|
+
n--;
|
|
565
|
+
result = String.fromCharCode(97 + (n % 26)) + result;
|
|
566
|
+
n = Math.floor(n / 26);
|
|
567
|
+
}
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const ROMAN_VALUES = [
|
|
572
|
+
[1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'],
|
|
573
|
+
[50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'],
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
function numberToRoman(n) {
|
|
577
|
+
let result = '';
|
|
578
|
+
for (const [value, numeral] of ROMAN_VALUES) {
|
|
579
|
+
while (n >= value) {
|
|
580
|
+
result += numeral;
|
|
581
|
+
n -= value;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function getListNumber(depth, orderedListNumber) {
|
|
588
|
+
switch (depth) {
|
|
589
|
+
case 0:
|
|
590
|
+
case 1:
|
|
591
|
+
return String(orderedListNumber);
|
|
592
|
+
case 2:
|
|
593
|
+
return numberToLetter(orderedListNumber);
|
|
594
|
+
case 3:
|
|
595
|
+
return numberToRoman(orderedListNumber);
|
|
596
|
+
default:
|
|
597
|
+
return String(orderedListNumber);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
56
601
|
/**
|
|
57
602
|
* Render a single marked token to an ANSI string.
|
|
58
|
-
*
|
|
603
|
+
* marked token switch (minus table / hyperlink deps).
|
|
59
604
|
*/
|
|
60
|
-
export function formatToken(token,
|
|
605
|
+
export function formatToken(token, listBaseIndent = 0, orderedListNumber = null, parent = null, width = 0, depth = 0) {
|
|
606
|
+
const { accent, codeBlock, headingAccent, quoteBorder, quoteText, hrLine } = colorizers();
|
|
607
|
+
const ex = extraColorizers();
|
|
61
608
|
switch (token.type) {
|
|
62
609
|
case 'blockquote': {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
610
|
+
const bar = quoteBorder(BLOCKQUOTE_BAR);
|
|
611
|
+
const quotePrefix = `${BLOCKQUOTE_BAR} `;
|
|
612
|
+
const innerWidth = contentWidthAfterPrefix(width, quotePrefix);
|
|
613
|
+
const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
|
|
65
614
|
return inner
|
|
66
615
|
.split(EOL)
|
|
67
|
-
.map((line) =>
|
|
616
|
+
.map((line) => {
|
|
617
|
+
// Padded fenced-code blank rows are space-only; trim() would drop the quote bar.
|
|
618
|
+
if (stringWidth(stripAnsi(line)) === 0) return line;
|
|
619
|
+
return `${bar} ${quoteText(chalk.italic(line))}`;
|
|
620
|
+
})
|
|
68
621
|
.join(EOL);
|
|
69
622
|
}
|
|
70
623
|
case 'code':
|
|
71
|
-
//
|
|
72
|
-
return token
|
|
624
|
+
// Fenced block: flush-left wrapped body with syntax highlighting.
|
|
625
|
+
return renderCodeBlock(token, width);
|
|
73
626
|
case 'codespan':
|
|
74
627
|
// inline code
|
|
75
|
-
return accent(decodeEntities(token.text));
|
|
628
|
+
return ex.codeSpanBg(accent(decodeEntities(token.text)));
|
|
76
629
|
case 'em':
|
|
77
|
-
return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
|
|
630
|
+
return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
|
|
78
631
|
case 'strong':
|
|
79
|
-
return chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
|
|
632
|
+
return ex.strong(chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
|
|
80
633
|
case 'heading':
|
|
81
634
|
switch (token.depth) {
|
|
82
635
|
case 1:
|
|
83
636
|
return (
|
|
84
|
-
chalk.bold.italic.underline(
|
|
85
|
-
(token.tokens ?? []).map((t) => formatToken(t)).join(''),
|
|
86
|
-
) + EOL + EOL
|
|
637
|
+
chalk.bold.italic.underline(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL
|
|
87
638
|
);
|
|
88
639
|
default: // h2+
|
|
89
|
-
return chalk.bold((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL;
|
|
640
|
+
return chalk.bold(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL;
|
|
90
641
|
}
|
|
91
|
-
case 'hr':
|
|
92
|
-
|
|
642
|
+
case 'hr': {
|
|
643
|
+
// Span the available content width with a box-drawing rule. width is only
|
|
644
|
+
// threaded from the top-level token loop; recursive calls pass 0 and fall
|
|
645
|
+
// back to a sane 80-col rule.
|
|
646
|
+
const w = Math.max(3, Number(width) || 80);
|
|
647
|
+
return hrLine(HR_LINE.repeat(w)) + EOL;
|
|
648
|
+
}
|
|
93
649
|
case 'image':
|
|
94
650
|
return token.href;
|
|
95
651
|
case 'link': {
|
|
96
652
|
if (token.href.startsWith('mailto:')) {
|
|
97
|
-
return token.href.replace(/^mailto:/, '');
|
|
653
|
+
return ex.linkText(token.href.replace(/^mailto:/, ''));
|
|
98
654
|
}
|
|
99
655
|
const linkText = (token.tokens ?? []).map((t) => formatToken(t, 0, null, token)).join('');
|
|
100
656
|
const plain = stripAnsi(linkText);
|
|
101
|
-
|
|
102
|
-
|
|
657
|
+
const href = token.href;
|
|
658
|
+
// OSC 8 hyperlink: clickable label, URL hidden in supporting terminals
|
|
659
|
+
// (Windows Terminal / iTerm); other terminals show the label text as-is.
|
|
660
|
+
// AnsiText only strips `\x1b[...m` SGR, so the OSC 8 `\x1b]8;;…\x07`
|
|
661
|
+
// sequences pass through untouched for the terminal to interpret.
|
|
662
|
+
const OSC8_OPEN = (url) => `\x1b]8;;${url}\x07`;
|
|
663
|
+
const OSC8_CLOSE = '\x1b]8;;\x07';
|
|
664
|
+
if (plain && plain !== href) {
|
|
665
|
+
const styledLabel = ex.linkText(chalk.underline(linkText));
|
|
666
|
+
return `${OSC8_OPEN(href)}${styledLabel}${OSC8_CLOSE}`;
|
|
103
667
|
}
|
|
104
|
-
|
|
668
|
+
// No distinct label (empty or equal to href): show the URL itself as the
|
|
669
|
+
// clickable, visible text.
|
|
670
|
+
return `${OSC8_OPEN(href)}${ex.link(chalk.underline(href))}${OSC8_CLOSE}`;
|
|
105
671
|
}
|
|
106
672
|
case 'list':
|
|
107
673
|
return token.items
|
|
108
674
|
.map((item, index) =>
|
|
109
|
-
formatToken(
|
|
675
|
+
formatToken(
|
|
676
|
+
item,
|
|
677
|
+
listBaseIndent,
|
|
678
|
+
token.ordered ? Number(token.start || 1) + index : null,
|
|
679
|
+
token,
|
|
680
|
+
width,
|
|
681
|
+
depth,
|
|
682
|
+
),
|
|
110
683
|
)
|
|
111
684
|
.join('');
|
|
112
685
|
case 'list_item':
|
|
113
|
-
return (token
|
|
114
|
-
.map((t) => `${' '.repeat(listDepth)}${formatToken(t, listDepth + 1, orderedListNumber, token)}`)
|
|
115
|
-
.join('');
|
|
686
|
+
return formatListItem(token, listBaseIndent, orderedListNumber, parent, depth, width);
|
|
116
687
|
case 'paragraph':
|
|
117
|
-
return (token.tokens ?? []).map((t) => formatToken(t)).join('') + EOL;
|
|
688
|
+
return (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, width)).join('') + EOL;
|
|
118
689
|
case 'space':
|
|
119
690
|
return EOL;
|
|
120
691
|
case 'br':
|
|
121
692
|
return EOL;
|
|
122
693
|
case 'text':
|
|
123
694
|
if (parent?.type === 'link') return decodeEntities(token.text);
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
const body = token.tokens
|
|
127
|
-
? token.tokens.map((t) => formatToken(t, listDepth, orderedListNumber, token)).join('')
|
|
128
|
-
: decodeEntities(token.text);
|
|
129
|
-
return `${marker} ${body}${EOL}`;
|
|
695
|
+
if (token.tokens) {
|
|
696
|
+
return token.tokens.map((t) => formatToken(t, listBaseIndent, orderedListNumber, token, width, depth)).join('');
|
|
130
697
|
}
|
|
131
698
|
return decodeEntities(token.text);
|
|
132
699
|
case 'escape':
|
|
@@ -140,44 +707,79 @@ export function formatToken(token, listDepth = 0, orderedListNumber = null, pare
|
|
|
140
707
|
}
|
|
141
708
|
}
|
|
142
709
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
function numberToLetter(n) {
|
|
146
|
-
let result = '';
|
|
147
|
-
while (n > 0) {
|
|
148
|
-
n--;
|
|
149
|
-
result = String.fromCharCode(97 + (n % 26)) + result;
|
|
150
|
-
n = Math.floor(n / 26);
|
|
151
|
-
}
|
|
152
|
-
return result;
|
|
710
|
+
function trimTrailingEol(value) {
|
|
711
|
+
return String(value ?? '').replace(/\n+$/g, '');
|
|
153
712
|
}
|
|
154
713
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
714
|
+
function prefixLines(value, prefix) {
|
|
715
|
+
return String(value ?? '')
|
|
716
|
+
.split(EOL)
|
|
717
|
+
.map((line) => `${prefix}${line}`)
|
|
718
|
+
.join(EOL);
|
|
719
|
+
}
|
|
159
720
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
721
|
+
function prefixFirstAndRest(value, firstPrefix, restPrefix) {
|
|
722
|
+
const lines = String(value ?? '').split(EOL);
|
|
723
|
+
if (lines.length === 0) return '';
|
|
724
|
+
return [
|
|
725
|
+
`${firstPrefix}${lines[0] ?? ''}`,
|
|
726
|
+
...lines.slice(1).map((line) => `${restPrefix}${line}`),
|
|
727
|
+
].join(EOL);
|
|
166
728
|
}
|
|
167
729
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
730
|
+
function formatListItem(token, listBaseIndent, orderedListNumber, parent, depth = 0, width = 0) {
|
|
731
|
+
const { listBullet } = colorizers();
|
|
732
|
+
const markerPlain = orderedListNumber === null
|
|
733
|
+
? '-'
|
|
734
|
+
: `${getListNumber(depth, orderedListNumber)}.`;
|
|
735
|
+
const marker = listBullet(markerPlain);
|
|
736
|
+
const markerPrefix = `${' '.repeat(listBaseIndent)}${marker} `;
|
|
737
|
+
const continuationPrefix = ' '.repeat(stripAnsi(markerPrefix).length);
|
|
738
|
+
const nestedListIndent = continuationPrefix.length;
|
|
739
|
+
const childWidth = contentWidthAfterPrefix(width, continuationPrefix);
|
|
740
|
+
const children = token.tokens ?? [];
|
|
741
|
+
let out = '';
|
|
742
|
+
let firstBlock = true;
|
|
743
|
+
|
|
744
|
+
for (const child of children) {
|
|
745
|
+
if (!child) continue;
|
|
746
|
+
|
|
747
|
+
if (child.type === 'space') {
|
|
748
|
+
if (!firstBlock) {
|
|
749
|
+
out += `${continuationPrefix}${EOL}`;
|
|
750
|
+
}
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (child.type === 'list') {
|
|
755
|
+
if (firstBlock) {
|
|
756
|
+
out += `${markerPrefix.trimEnd()}${EOL}`;
|
|
757
|
+
firstBlock = false;
|
|
758
|
+
}
|
|
759
|
+
// Pass outer `width` so the nested list subtracts only its own marker prefix once.
|
|
760
|
+
out += formatToken(child, nestedListIndent, null, token, width, depth + 1);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const rendered = formatToken(child, listBaseIndent, orderedListNumber, token, childWidth, depth);
|
|
765
|
+
const body = trimTrailingEol(rendered);
|
|
766
|
+
if (!body) continue;
|
|
767
|
+
|
|
768
|
+
if (firstBlock) {
|
|
769
|
+
out += `${prefixFirstAndRest(body, markerPrefix, continuationPrefix)}${EOL}`;
|
|
770
|
+
firstBlock = false;
|
|
771
|
+
} else {
|
|
772
|
+
out += `${prefixLines(body, continuationPrefix)}${EOL}`;
|
|
773
|
+
}
|
|
175
774
|
}
|
|
775
|
+
|
|
776
|
+
if (firstBlock) return `${markerPrefix.trimEnd()}${EOL}`;
|
|
777
|
+
return out;
|
|
176
778
|
}
|
|
177
779
|
|
|
178
780
|
/**
|
|
179
781
|
* Pad `content` to `targetWidth` per alignment. `displayWidth` is the visible
|
|
180
|
-
* width of `content` (ANSI codes excluded).
|
|
782
|
+
* width of `content` (ANSI codes excluded).
|
|
181
783
|
*/
|
|
182
784
|
export function padAligned(content, displayWidth, targetWidth, align) {
|
|
183
785
|
const padding = Math.max(0, targetWidth - displayWidth);
|