mixdog 0.8.0 → 0.9.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/README.md +47 -23
- package/package.json +33 -27
- 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 +344 -0
- 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 +1223 -95
- 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 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- 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 +118 -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 +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -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 +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- 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 +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- 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 +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- 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 +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- 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/snapshot-store.mjs +1 -1
- 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 +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- 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/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- 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 +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -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 +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -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 +145 -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 -98
- 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 +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- 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 +367 -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 +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- 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 +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -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 +746 -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 +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- 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/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/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/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,29 @@
|
|
|
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) is emitted as plain text + EOL (no
|
|
10
|
-
* dependency); `codespan` (inline) gets the accent color
|
|
8
|
+
* - `code` (fenced) is emitted as a block-colored plain text + EOL (no
|
|
9
|
+
* syntax highlighter dependency); `codespan` (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';
|
|
17
16
|
import stripAnsi from 'strip-ansi';
|
|
18
|
-
import
|
|
19
|
-
import
|
|
17
|
+
import stringWidth from 'string-width';
|
|
18
|
+
import wrapAnsi from 'wrap-ansi';
|
|
19
|
+
import { theme, getThemeVersion } from '../theme.mjs';
|
|
20
|
+
import { BLOCKQUOTE_BAR, HR_LINE } from '../figures.mjs';
|
|
20
21
|
|
|
21
22
|
// Force truecolor so chalk emits 24-bit SGR even when the ambient level is 0.
|
|
22
23
|
// ink's <Text> passes these escapes through verbatim.
|
|
23
24
|
const chalk = new Chalk({ level: 3 });
|
|
24
25
|
|
|
25
|
-
// Use \n unconditionally (
|
|
26
|
+
// Use \n unconditionally (os.EOL's \r breaks segment mapping).
|
|
26
27
|
const EOL = '\n';
|
|
27
28
|
|
|
28
29
|
/** Parse an `rgb(r,g,b)` theme string into a chalk colorizer. */
|
|
@@ -32,8 +33,77 @@ function rgbColor(str) {
|
|
|
32
33
|
return chalk.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Parse an `rgb(r,g,b)` theme string into a TRUECOLOR BACKGROUND wrapper. Emits
|
|
38
|
+
* `48;2;R;G;B` … `49` (bg reset) around the string so AnsiText (case 48) maps it
|
|
39
|
+
* to an ink backgroundColor, giving a code line a tinted band. Returns identity
|
|
40
|
+
* when the string is not a valid rgb() so a missing key never corrupts output.
|
|
41
|
+
*/
|
|
42
|
+
function rgbBg(str) {
|
|
43
|
+
const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(str || ''));
|
|
44
|
+
if (!m) return (s) => s;
|
|
45
|
+
const open = `\x1b[48;2;${+m[1]};${+m[2]};${+m[3]}m`;
|
|
46
|
+
return (s) => `${open}${s}\x1b[49m`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Colorizers are derived from the active theme's md* keys. They are cached and
|
|
50
|
+
// rebuilt only when the theme version changes, so a `/theme` switch takes
|
|
51
|
+
// effect on the next render without recomputing a chalk fn per token.
|
|
52
|
+
let _colorizerVersion = -1;
|
|
53
|
+
let _colorizers = null;
|
|
54
|
+
function colorizers() {
|
|
55
|
+
const version = getThemeVersion();
|
|
56
|
+
if (_colorizers && _colorizerVersion === version) return _colorizers;
|
|
57
|
+
_colorizerVersion = version;
|
|
58
|
+
_colorizers = {
|
|
59
|
+
accent: rgbColor(theme.mdCode), // inline codespan
|
|
60
|
+
codeBlock: rgbColor(theme.mdCodeBlock), // fenced code block body
|
|
61
|
+
headingAccent: rgbColor(theme.mdHeading),
|
|
62
|
+
quoteBorder: rgbColor(theme.mdQuoteBorder),
|
|
63
|
+
quoteText: rgbColor(theme.mdQuote),
|
|
64
|
+
hrLine: rgbColor(theme.mdHr),
|
|
65
|
+
listBullet: rgbColor(theme.mdListBullet),
|
|
66
|
+
};
|
|
67
|
+
return _colorizers;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extended colorizers (fence border, link, emphasis, diff + syntax classes).
|
|
71
|
+
// Cached on the same theme-version key so a /theme switch rebuilds both maps.
|
|
72
|
+
let _extraVersion = -1;
|
|
73
|
+
let _extra = null;
|
|
74
|
+
export function extraColorizers() {
|
|
75
|
+
const version = getThemeVersion();
|
|
76
|
+
if (_extra && _extraVersion === version) return _extra;
|
|
77
|
+
_extraVersion = version;
|
|
78
|
+
const fallbackBody = rgbColor(theme.mdCodeBlock);
|
|
79
|
+
_extra = {
|
|
80
|
+
fenceBorder: rgbColor(theme.mdCodeBlockBorder ?? theme.mdHr),
|
|
81
|
+
link: rgbColor(theme.mdLink ?? theme.code),
|
|
82
|
+
linkText: rgbColor(theme.mdLinkText ?? theme.mdCode),
|
|
83
|
+
strong: rgbColor(theme.mdStrong ?? theme.mdHeading),
|
|
84
|
+
emph: rgbColor(theme.mdEmph ?? theme.mdHeading),
|
|
85
|
+
// diff/patch
|
|
86
|
+
diffAdded: rgbColor(theme.mdDiffAdded ?? theme.success),
|
|
87
|
+
diffRemoved: rgbColor(theme.mdDiffRemoved ?? theme.error),
|
|
88
|
+
diffHunk: rgbColor(theme.mdDiffHunk ?? theme.code),
|
|
89
|
+
diffHeader: rgbColor(theme.mdDiffHeader ?? theme.mdHeading),
|
|
90
|
+
diffContext: rgbColor(theme.mdDiffContext ?? theme.subtle),
|
|
91
|
+
// syntax classes (fall back to plain code-block body when key missing)
|
|
92
|
+
synComment: rgbColor(theme.syntaxComment ?? theme.subtle),
|
|
93
|
+
synKeyword: rgbColor(theme.syntaxKeyword ?? theme.mdCodeBlock),
|
|
94
|
+
synFunction: rgbColor(theme.syntaxFunction ?? theme.mdCodeBlock),
|
|
95
|
+
synVariable: rgbColor(theme.syntaxVariable ?? theme.mdCodeBlock),
|
|
96
|
+
synString: rgbColor(theme.syntaxString ?? theme.mdCodeBlock),
|
|
97
|
+
synNumber: rgbColor(theme.syntaxNumber ?? theme.mdCodeBlock),
|
|
98
|
+
synType: rgbColor(theme.syntaxType ?? theme.mdCodeBlock),
|
|
99
|
+
synOperator: rgbColor(theme.syntaxOperator ?? theme.mdCodeBlock),
|
|
100
|
+
synPunct: rgbColor(theme.syntaxPunctuation ?? theme.mdCodeBlock),
|
|
101
|
+
body: fallbackBody,
|
|
102
|
+
// Truecolor background band for fenced code blocks (per-line wrap).
|
|
103
|
+
codeBg: rgbBg(theme.mdCodeBlockBg ?? theme.background),
|
|
104
|
+
};
|
|
105
|
+
return _extra;
|
|
106
|
+
}
|
|
37
107
|
|
|
38
108
|
// marked 14 HTML-encodes token.text / codespan.text (`"` → `"`, `&` →
|
|
39
109
|
// `&`, `<`→`<`, `>`→`>`, `'`→`'`), but we render to a terminal,
|
|
@@ -53,80 +123,419 @@ function decodeEntities(s) {
|
|
|
53
123
|
.replace(/&/g, '&');
|
|
54
124
|
}
|
|
55
125
|
|
|
126
|
+
// ── Fenced code block rendering ─────────────────────────────────────────────
|
|
127
|
+
// Compact language label (no ``` fences), two-space-indented wrapped body.
|
|
128
|
+
// Diff/patch languages
|
|
129
|
+
// (and code text that clearly looks like a unified diff) get colored +/-/@@
|
|
130
|
+
// lines; other known languages get a conservative regex highlighter; unknown
|
|
131
|
+
// languages fall back to the flat `mdCodeBlock` body color.
|
|
132
|
+
const CODE_BLOCK_INDENT = ' ';
|
|
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, no indent) and prefix each segment. */
|
|
171
|
+
function wrapIndentedCodeLine(ansiContent, maxLineWidth) {
|
|
172
|
+
const indentW = stringWidth(CODE_BLOCK_INDENT);
|
|
173
|
+
const contentMax = Math.max(1, maxLineWidth - indentW);
|
|
174
|
+
const segments = hardWrapAnsiLines(ansiContent, contentMax);
|
|
175
|
+
return segments.map((seg) => `${CODE_BLOCK_INDENT}${seg}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Tint code lines without padding to terminal width (no trailing bg band). */
|
|
179
|
+
function tintCodeLines(lines, codeBg) {
|
|
180
|
+
return lines.map((line) => codeBg(line)).join(EOL);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
|
|
184
|
+
function contentWidthAfterPrefix(width, prefix) {
|
|
185
|
+
const base = Number(width) || 0;
|
|
186
|
+
if (base <= 0) return 0;
|
|
187
|
+
return Math.max(8, base - stringWidth(String(prefix ?? '')));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Normalize a fenced info-string to a bare lowercase language token. */
|
|
191
|
+
function normalizeLang(lang) {
|
|
192
|
+
return String(lang || '')
|
|
193
|
+
.trim()
|
|
194
|
+
.split(/\s+/)[0]
|
|
195
|
+
.toLowerCase();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Heuristic: does this code body look like a unified diff/patch? Requires at
|
|
200
|
+
* least one +/- body line AND a hunk header or file header, so ordinary code
|
|
201
|
+
* with leading +/- (rare) is not misclassified.
|
|
202
|
+
*/
|
|
203
|
+
export function looksLikeUnifiedDiff(text) {
|
|
204
|
+
const lines = String(text ?? '').split(EOL);
|
|
205
|
+
let hasHunk = false;
|
|
206
|
+
let hasFileHeader = false;
|
|
207
|
+
let hasSign = false;
|
|
208
|
+
let hasStat = false;
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
if (/^@@ .* @@/.test(line) || /^@@ /.test(line)) hasHunk = true;
|
|
211
|
+
if (/^(\+\+\+ |--- |diff --git |index [0-9a-f]+)/.test(line)) hasFileHeader = true;
|
|
212
|
+
if (/^[+-](?![+-])/.test(line)) hasSign = true;
|
|
213
|
+
// `git diff --stat` summary rows: `path | 4 +-` and the trailer
|
|
214
|
+
// `N files changed, M insertions(+), K deletions(-)`.
|
|
215
|
+
if (DIFF_STAT_FILE_RE.test(line) || DIFF_STAT_SUMMARY_RE.test(line)) hasStat = true;
|
|
216
|
+
}
|
|
217
|
+
return ((hasHunk || hasFileHeader) && hasSign) || hasStat;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// `git diff --stat` rows. FILE: `<path> | <count> <+/-/ graph>` (count may be
|
|
221
|
+
// `Bin`); SUMMARY: `N file(s) changed[, M insertion…][, K deletion…]`.
|
|
222
|
+
const DIFF_STAT_FILE_RE = /^(\s*)(.+?)(\s+\|\s+)(Bin\b.*|\d+\s*[+\-]*)\s*$/;
|
|
223
|
+
const DIFF_STAT_SUMMARY_RE = /^\s*\d+\s+files?\s+changed\b/;
|
|
224
|
+
|
|
225
|
+
/** Color a `git diff --stat` file row: dim path/sep, green `+`, red `-`. */
|
|
226
|
+
function colorizeDiffStatLine(line, c) {
|
|
227
|
+
const m = DIFF_STAT_FILE_RE.exec(line);
|
|
228
|
+
if (m) {
|
|
229
|
+
const [, indent, path, sep, tail] = m;
|
|
230
|
+
// The tail is either `Bin …` or `<count> <graph>` where graph is +/-.
|
|
231
|
+
const countMatch = /^(\d+)(\s*)([+\-]*)\s*$/.exec(tail);
|
|
232
|
+
let coloredTail;
|
|
233
|
+
if (countMatch) {
|
|
234
|
+
const [, count, gap, bars] = countMatch;
|
|
235
|
+
const pluses = c.diffAdded('+'.repeat((bars.match(/\+/g) || []).length));
|
|
236
|
+
const minuses = c.diffRemoved('-'.repeat((bars.match(/-/g) || []).length));
|
|
237
|
+
coloredTail = `${c.diffContext(count)}${gap}${pluses}${minuses}`;
|
|
238
|
+
} else {
|
|
239
|
+
coloredTail = c.diffContext(tail);
|
|
240
|
+
}
|
|
241
|
+
return `${indent}${c.diffContext(path)}${c.diffContext(sep)}${coloredTail}`;
|
|
242
|
+
}
|
|
243
|
+
return c.diffContext(line);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Classify and color a single unified-diff line. */
|
|
247
|
+
export function colorizeDiffLine(line, c) {
|
|
248
|
+
// File/section headers first so +++/--- are never treated as add/remove.
|
|
249
|
+
if (/^(\+\+\+|---)(\s|$)/.test(line)) return c.diffHeader(line);
|
|
250
|
+
if (/^(diff --git |index [0-9a-f]|new file|deleted file|rename |similarity |old mode|new mode)/.test(line)) {
|
|
251
|
+
return c.diffHeader(line);
|
|
252
|
+
}
|
|
253
|
+
if (/^@@/.test(line)) return c.diffHunk(line);
|
|
254
|
+
if (/^\+/.test(line)) return c.diffAdded(line);
|
|
255
|
+
if (/^-/.test(line)) return c.diffRemoved(line);
|
|
256
|
+
if (/^\\ No newline/.test(line)) return c.diffContext(line);
|
|
257
|
+
// `git diff --stat` summary trailer + per-file rows.
|
|
258
|
+
if (DIFF_STAT_SUMMARY_RE.test(line)) return colorizeDiffStatTrailer(line, c);
|
|
259
|
+
if (DIFF_STAT_FILE_RE.test(line)) return colorizeDiffStatLine(line, c);
|
|
260
|
+
return c.diffContext(line);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Color the `N files changed, M insertions(+), K deletions(-)` trailer. */
|
|
264
|
+
function colorizeDiffStatTrailer(line, c) {
|
|
265
|
+
return line.replace(/(\d+)(\s+insertions?\(\+\))/g, (_, n, rest) => `${c.diffAdded(n)}${c.diffContext(rest)}`)
|
|
266
|
+
.replace(/(\d+)(\s+deletions?\(-\))/g, (_, n, rest) => `${c.diffRemoved(n)}${c.diffContext(rest)}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderDiffBody(text, c, bandWidth) {
|
|
270
|
+
const lines = [];
|
|
271
|
+
for (const line of String(text ?? '').split(EOL)) {
|
|
272
|
+
const colored = colorizeDiffLine(line, c);
|
|
273
|
+
lines.push(...wrapIndentedCodeLine(colored, bandWidth));
|
|
274
|
+
}
|
|
275
|
+
return tintCodeLines(lines, c.codeBg);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Lightweight syntax highlighting (regex token classes, no parser) ────────
|
|
279
|
+
const KEYWORDS = {
|
|
280
|
+
js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'new', 'class', 'extends', 'super', 'this', 'import', 'export', 'from', 'default', 'async', 'await', 'yield', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'void', 'delete', 'null', 'undefined', 'true', 'false'],
|
|
281
|
+
py: ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import', 'from', 'as', 'with', 'try', 'except', 'finally', 'raise', 'pass', 'break', 'continue', 'lambda', 'yield', 'global', 'nonlocal', 'async', 'await', 'and', 'or', 'not', 'in', 'is', 'None', 'True', 'False', 'self'],
|
|
282
|
+
sh: ['if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'do', 'done', 'case', 'esac', 'function', 'return', 'in', 'export', 'local', 'echo', 'cd', 'set', 'unset', 'read', 'source'],
|
|
283
|
+
css: [],
|
|
284
|
+
go: ['package', 'import', 'func', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'default', 'break', 'continue', 'fallthrough', 'goto', 'defer', 'go', 'select', 'chan', 'map', 'struct', 'interface', 'type', 'var', 'const', 'nil', 'true', 'false', 'iota', 'string', 'int', 'int64', 'float64', 'bool', 'byte', 'rune', 'error'],
|
|
285
|
+
rust: ['fn', 'let', 'mut', 'const', 'static', 'return', 'if', 'else', 'match', 'for', 'while', 'loop', 'break', 'continue', 'struct', 'enum', 'trait', 'impl', 'mod', 'use', 'pub', 'crate', 'self', 'super', 'where', 'as', 'dyn', 'move', 'ref', 'unsafe', 'async', 'await', 'type', 'true', 'false', 'Some', 'None', 'Ok', 'Err', 'Box', 'Vec', 'String', 'usize', 'isize', 'i32', 'u32', 'i64', 'u64', 'f64', 'bool'],
|
|
286
|
+
java: ['public', 'private', 'protected', 'class', 'interface', 'enum', 'extends', 'implements', 'abstract', 'final', 'static', 'void', 'new', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'this', 'super', 'instanceof', 'synchronized', 'volatile', 'transient', 'native', 'int', 'long', 'short', 'byte', 'char', 'float', 'double', 'boolean', 'true', 'false', 'null'],
|
|
287
|
+
c: ['auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 'inline', 'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', 'class', 'namespace', 'template', 'public', 'private', 'protected', 'virtual', 'new', 'delete', 'using', 'nullptr', 'true', 'false', 'bool'],
|
|
288
|
+
ruby: ['def', 'end', 'class', 'module', 'if', 'elsif', 'else', 'unless', 'case', 'when', 'then', 'while', 'until', 'for', 'in', 'do', 'begin', 'rescue', 'ensure', 'raise', 'return', 'yield', 'break', 'next', 'redo', 'retry', 'self', 'nil', 'true', 'false', 'and', 'or', 'not', 'require', 'require_relative', 'attr_accessor', 'attr_reader', 'attr_writer', 'puts', 'lambda', 'proc'],
|
|
289
|
+
sql: ['select', 'from', 'where', 'insert', 'into', 'values', 'update', 'set', 'delete', 'create', 'alter', 'drop', 'table', 'view', 'index', 'join', 'inner', 'left', 'right', 'outer', 'full', 'on', 'group', 'by', 'order', 'having', 'limit', 'offset', 'distinct', 'as', 'and', 'or', 'not', 'null', 'is', 'in', 'between', 'like', 'union', 'all', 'primary', 'key', 'foreign', 'references', 'default', 'constraint', 'unique', 'count', 'sum', 'avg', 'min', 'max'],
|
|
290
|
+
yaml: [],
|
|
291
|
+
toml: [],
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const LANG_FAMILY = {
|
|
295
|
+
js: 'js', javascript: 'js', mjs: 'js', cjs: 'js',
|
|
296
|
+
ts: 'js', typescript: 'js', jsx: 'js', tsx: 'js',
|
|
297
|
+
json: 'json', json5: 'json',
|
|
298
|
+
bash: 'sh', sh: 'sh', shell: 'sh', zsh: 'sh',
|
|
299
|
+
python: 'py', py: 'py',
|
|
300
|
+
css: 'css', scss: 'css', less: 'css',
|
|
301
|
+
html: 'html', xml: 'html',
|
|
302
|
+
md: 'md', markdown: 'md',
|
|
303
|
+
go: 'go', golang: 'go',
|
|
304
|
+
rust: 'rust', rs: 'rust',
|
|
305
|
+
java: 'java',
|
|
306
|
+
c: 'c', cpp: 'c', 'c++': 'c', cc: 'c', h: 'c', hpp: 'c',
|
|
307
|
+
ruby: 'ruby', rb: 'ruby',
|
|
308
|
+
sql: 'sql',
|
|
309
|
+
yaml: 'yaml', yml: 'yaml',
|
|
310
|
+
toml: 'toml',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Families whose comment lines / inline comments start with `#`.
|
|
314
|
+
const HASH_COMMENT_FAMILIES = new Set(['sh', 'py', 'yaml', 'toml']);
|
|
315
|
+
|
|
316
|
+
/** Highlight a single line for a c-like / scripting family (token scan). */
|
|
317
|
+
export function highlightCodeLine(line, family, c) {
|
|
318
|
+
const kw = new Set(KEYWORDS[family === 'json' ? 'js' : family] ?? []);
|
|
319
|
+
// Comment lines (whole-line) for the common families.
|
|
320
|
+
if (family === 'js' && /^\s*\/\//.test(line)) return c.synComment(line);
|
|
321
|
+
if (HASH_COMMENT_FAMILIES.has(family) && /^\s*#/.test(line)) return c.synComment(line);
|
|
322
|
+
if (family === 'html' && /^\s*<!--/.test(line)) return c.synComment(line);
|
|
323
|
+
|
|
324
|
+
// Token regex: strings, numbers, comments, identifiers, punctuation.
|
|
325
|
+
const TOKEN_RE = /(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(\/\/[^\n]*|#[^\n]*)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][A-Za-z0-9_$]*)|([{}()[\].,;:=+\-*/%<>!&|^~?]+)/g;
|
|
326
|
+
let out = '';
|
|
327
|
+
let last = 0;
|
|
328
|
+
let m;
|
|
329
|
+
TOKEN_RE.lastIndex = 0;
|
|
330
|
+
while ((m = TOKEN_RE.exec(line)) !== null) {
|
|
331
|
+
if (m.index > last) out += c.body(line.slice(last, m.index));
|
|
332
|
+
const [, str, comment, num, ident, punct] = m;
|
|
333
|
+
if (str !== undefined) {
|
|
334
|
+
out += c.synString(str);
|
|
335
|
+
} else if (comment !== undefined) {
|
|
336
|
+
// `#` is only a comment for sh/py; for js treat as body.
|
|
337
|
+
if (comment.startsWith('//') ? family === 'js' : HASH_COMMENT_FAMILIES.has(family)) {
|
|
338
|
+
out += c.synComment(comment);
|
|
339
|
+
} else {
|
|
340
|
+
out += c.body(comment);
|
|
341
|
+
}
|
|
342
|
+
} else if (num !== undefined) {
|
|
343
|
+
out += c.synNumber(num);
|
|
344
|
+
} else if (ident !== undefined) {
|
|
345
|
+
if (kw.has(ident)) out += c.synKeyword(ident);
|
|
346
|
+
else if (line[TOKEN_RE.lastIndex] === '(') out += c.synFunction(ident);
|
|
347
|
+
else out += c.body(ident);
|
|
348
|
+
} else if (punct !== undefined) {
|
|
349
|
+
out += c.synOperator(punct);
|
|
350
|
+
}
|
|
351
|
+
last = TOKEN_RE.lastIndex;
|
|
352
|
+
}
|
|
353
|
+
if (last < line.length) out += c.body(line.slice(last));
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function renderHighlightedBody(text, family, c, bandWidth) {
|
|
358
|
+
const lines = [];
|
|
359
|
+
for (const line of String(text ?? '').split(EOL)) {
|
|
360
|
+
const colored = line ? highlightCodeLine(line, family, c) : '';
|
|
361
|
+
lines.push(...wrapIndentedCodeLine(colored, bandWidth));
|
|
362
|
+
}
|
|
363
|
+
return tintCodeLines(lines, c.codeBg);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Render a fenced `code` token: compact language label, wrapped indented body,
|
|
368
|
+
* and language-aware coloring (no visible ``` fence lines).
|
|
369
|
+
*/
|
|
370
|
+
function renderCodeBlock(token, width = 0) {
|
|
371
|
+
const { codeBlock } = colorizers();
|
|
372
|
+
const c = extraColorizers();
|
|
373
|
+
const lang = normalizeLang(token.lang);
|
|
374
|
+
const text = decodeEntities(token.text ?? '');
|
|
375
|
+
const bandWidth = Math.max(8, Number(width) || 80);
|
|
376
|
+
|
|
377
|
+
let body;
|
|
378
|
+
if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
|
|
379
|
+
body = renderDiffBody(text, c, bandWidth);
|
|
380
|
+
} else {
|
|
381
|
+
const family = LANG_FAMILY[lang];
|
|
382
|
+
// Any known family except markdown-in-markdown routes through the regex
|
|
383
|
+
// highlighter (html/json/yaml/toml included). 'md' stays flat (no nested
|
|
384
|
+
// markdown highlighter); unknown langs fall back to the flat body color.
|
|
385
|
+
if (family && family !== 'md') {
|
|
386
|
+
body = renderHighlightedBody(text, family, c, bandWidth);
|
|
387
|
+
} else {
|
|
388
|
+
// Unknown/plain language: flat code-block body color, still indented.
|
|
389
|
+
const lines = [];
|
|
390
|
+
for (const line of text.split(EOL)) {
|
|
391
|
+
const colored = codeBlock(line);
|
|
392
|
+
lines.push(...wrapIndentedCodeLine(colored, bandWidth));
|
|
393
|
+
}
|
|
394
|
+
body = tintCodeLines(lines, c.codeBg);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const langLine = lang
|
|
398
|
+
? `${c.codeBg(c.fenceBorder(lang))}${EOL}`
|
|
399
|
+
: '';
|
|
400
|
+
return `${langLine}${body}${body ? EOL : ''}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function numberToLetter(n) {
|
|
404
|
+
let result = '';
|
|
405
|
+
while (n > 0) {
|
|
406
|
+
n--;
|
|
407
|
+
result = String.fromCharCode(97 + (n % 26)) + result;
|
|
408
|
+
n = Math.floor(n / 26);
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const ROMAN_VALUES = [
|
|
414
|
+
[1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'],
|
|
415
|
+
[50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'],
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
function numberToRoman(n) {
|
|
419
|
+
let result = '';
|
|
420
|
+
for (const [value, numeral] of ROMAN_VALUES) {
|
|
421
|
+
while (n >= value) {
|
|
422
|
+
result += numeral;
|
|
423
|
+
n -= value;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function getListNumber(depth, orderedListNumber) {
|
|
430
|
+
switch (depth) {
|
|
431
|
+
case 0:
|
|
432
|
+
case 1:
|
|
433
|
+
return String(orderedListNumber);
|
|
434
|
+
case 2:
|
|
435
|
+
return numberToLetter(orderedListNumber);
|
|
436
|
+
case 3:
|
|
437
|
+
return numberToRoman(orderedListNumber);
|
|
438
|
+
default:
|
|
439
|
+
return String(orderedListNumber);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
56
443
|
/**
|
|
57
444
|
* Render a single marked token to an ANSI string.
|
|
58
|
-
*
|
|
445
|
+
* marked token switch (minus table / hyperlink deps).
|
|
59
446
|
*/
|
|
60
|
-
export function formatToken(token,
|
|
447
|
+
export function formatToken(token, listBaseIndent = 0, orderedListNumber = null, parent = null, width = 0, depth = 0) {
|
|
448
|
+
const { accent, codeBlock, headingAccent, quoteBorder, quoteText, hrLine } = colorizers();
|
|
449
|
+
const ex = extraColorizers();
|
|
61
450
|
switch (token.type) {
|
|
62
451
|
case 'blockquote': {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
452
|
+
const bar = quoteBorder(BLOCKQUOTE_BAR);
|
|
453
|
+
const quotePrefix = `${BLOCKQUOTE_BAR} `;
|
|
454
|
+
const innerWidth = contentWidthAfterPrefix(width, quotePrefix);
|
|
455
|
+
const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
|
|
65
456
|
return inner
|
|
66
457
|
.split(EOL)
|
|
67
|
-
.map((line) =>
|
|
458
|
+
.map((line) =>
|
|
459
|
+
stripAnsi(line).trim()
|
|
460
|
+
? `${bar} ${quoteText(chalk.italic(line))}`
|
|
461
|
+
: line,
|
|
462
|
+
)
|
|
68
463
|
.join(EOL);
|
|
69
464
|
}
|
|
70
465
|
case 'code':
|
|
71
|
-
//
|
|
72
|
-
return token
|
|
466
|
+
// Fenced block: compact lang label + wrapped indented, colored body.
|
|
467
|
+
return renderCodeBlock(token, width);
|
|
73
468
|
case 'codespan':
|
|
74
469
|
// inline code
|
|
75
470
|
return accent(decodeEntities(token.text));
|
|
76
471
|
case 'em':
|
|
77
|
-
return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
|
|
472
|
+
return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
|
|
78
473
|
case 'strong':
|
|
79
|
-
return chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
|
|
474
|
+
return ex.strong(chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
|
|
80
475
|
case 'heading':
|
|
81
476
|
switch (token.depth) {
|
|
82
477
|
case 1:
|
|
83
478
|
return (
|
|
84
|
-
chalk.bold.italic.underline(
|
|
85
|
-
(token.tokens ?? []).map((t) => formatToken(t)).join(''),
|
|
86
|
-
) + EOL + EOL
|
|
479
|
+
chalk.bold.italic.underline(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL
|
|
87
480
|
);
|
|
88
481
|
default: // h2+
|
|
89
|
-
return chalk.bold((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL;
|
|
482
|
+
return chalk.bold(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL;
|
|
90
483
|
}
|
|
91
|
-
case 'hr':
|
|
92
|
-
|
|
484
|
+
case 'hr': {
|
|
485
|
+
// Span the available content width with a box-drawing rule. width is only
|
|
486
|
+
// threaded from the top-level token loop; recursive calls pass 0 and fall
|
|
487
|
+
// back to a sane 80-col rule.
|
|
488
|
+
const w = Math.max(3, Number(width) || 80);
|
|
489
|
+
return hrLine(HR_LINE.repeat(w)) + EOL;
|
|
490
|
+
}
|
|
93
491
|
case 'image':
|
|
94
492
|
return token.href;
|
|
95
493
|
case 'link': {
|
|
96
494
|
if (token.href.startsWith('mailto:')) {
|
|
97
|
-
return token.href.replace(/^mailto:/, '');
|
|
495
|
+
return ex.linkText(token.href.replace(/^mailto:/, ''));
|
|
98
496
|
}
|
|
99
497
|
const linkText = (token.tokens ?? []).map((t) => formatToken(t, 0, null, token)).join('');
|
|
100
498
|
const plain = stripAnsi(linkText);
|
|
101
|
-
|
|
102
|
-
|
|
499
|
+
const href = token.href;
|
|
500
|
+
// OSC 8 hyperlink: clickable label, URL hidden in supporting terminals
|
|
501
|
+
// (Windows Terminal / iTerm); other terminals show the label text as-is.
|
|
502
|
+
// AnsiText only strips `\x1b[...m` SGR, so the OSC 8 `\x1b]8;;…\x07`
|
|
503
|
+
// sequences pass through untouched for the terminal to interpret.
|
|
504
|
+
const OSC8_OPEN = (url) => `\x1b]8;;${url}\x07`;
|
|
505
|
+
const OSC8_CLOSE = '\x1b]8;;\x07';
|
|
506
|
+
if (plain && plain !== href) {
|
|
507
|
+
const styledLabel = ex.linkText(chalk.underline(linkText));
|
|
508
|
+
return `${OSC8_OPEN(href)}${styledLabel}${OSC8_CLOSE}`;
|
|
103
509
|
}
|
|
104
|
-
|
|
510
|
+
// No distinct label (empty or equal to href): show the URL itself as the
|
|
511
|
+
// clickable, visible text.
|
|
512
|
+
return `${OSC8_OPEN(href)}${ex.link(chalk.underline(href))}${OSC8_CLOSE}`;
|
|
105
513
|
}
|
|
106
514
|
case 'list':
|
|
107
515
|
return token.items
|
|
108
516
|
.map((item, index) =>
|
|
109
|
-
formatToken(
|
|
517
|
+
formatToken(
|
|
518
|
+
item,
|
|
519
|
+
listBaseIndent,
|
|
520
|
+
token.ordered ? Number(token.start || 1) + index : null,
|
|
521
|
+
token,
|
|
522
|
+
width,
|
|
523
|
+
depth,
|
|
524
|
+
),
|
|
110
525
|
)
|
|
111
526
|
.join('');
|
|
112
527
|
case 'list_item':
|
|
113
|
-
return (token
|
|
114
|
-
.map((t) => `${' '.repeat(listDepth)}${formatToken(t, listDepth + 1, orderedListNumber, token)}`)
|
|
115
|
-
.join('');
|
|
528
|
+
return formatListItem(token, listBaseIndent, orderedListNumber, parent, depth, width);
|
|
116
529
|
case 'paragraph':
|
|
117
|
-
return (token.tokens ?? []).map((t) => formatToken(t)).join('') + EOL;
|
|
530
|
+
return (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, width)).join('') + EOL;
|
|
118
531
|
case 'space':
|
|
119
532
|
return EOL;
|
|
120
533
|
case 'br':
|
|
121
534
|
return EOL;
|
|
122
535
|
case 'text':
|
|
123
536
|
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}`;
|
|
537
|
+
if (token.tokens) {
|
|
538
|
+
return token.tokens.map((t) => formatToken(t, listBaseIndent, orderedListNumber, token, width, depth)).join('');
|
|
130
539
|
}
|
|
131
540
|
return decodeEntities(token.text);
|
|
132
541
|
case 'escape':
|
|
@@ -140,44 +549,78 @@ export function formatToken(token, listDepth = 0, orderedListNumber = null, pare
|
|
|
140
549
|
}
|
|
141
550
|
}
|
|
142
551
|
|
|
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;
|
|
552
|
+
function trimTrailingEol(value) {
|
|
553
|
+
return String(value ?? '').replace(/\n+$/g, '');
|
|
153
554
|
}
|
|
154
555
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
556
|
+
function prefixLines(value, prefix) {
|
|
557
|
+
return String(value ?? '')
|
|
558
|
+
.split(EOL)
|
|
559
|
+
.map((line) => `${prefix}${line}`)
|
|
560
|
+
.join(EOL);
|
|
561
|
+
}
|
|
159
562
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
563
|
+
function prefixFirstAndRest(value, firstPrefix, restPrefix) {
|
|
564
|
+
const lines = String(value ?? '').split(EOL);
|
|
565
|
+
if (lines.length === 0) return '';
|
|
566
|
+
return [
|
|
567
|
+
`${firstPrefix}${lines[0] ?? ''}`,
|
|
568
|
+
...lines.slice(1).map((line) => `${restPrefix}${line}`),
|
|
569
|
+
].join(EOL);
|
|
166
570
|
}
|
|
167
571
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
572
|
+
function formatListItem(token, listBaseIndent, orderedListNumber, parent, depth = 0, width = 0) {
|
|
573
|
+
const { listBullet } = colorizers();
|
|
574
|
+
const markerPlain = orderedListNumber === null
|
|
575
|
+
? '-'
|
|
576
|
+
: `${getListNumber(depth, orderedListNumber)}.`;
|
|
577
|
+
const marker = listBullet(markerPlain);
|
|
578
|
+
const markerPrefix = `${' '.repeat(listBaseIndent)}${marker} `;
|
|
579
|
+
const continuationPrefix = ' '.repeat(stripAnsi(markerPrefix).length);
|
|
580
|
+
const nestedListIndent = continuationPrefix.length;
|
|
581
|
+
const childWidth = contentWidthAfterPrefix(width, continuationPrefix);
|
|
582
|
+
const children = token.tokens ?? [];
|
|
583
|
+
let out = '';
|
|
584
|
+
let firstBlock = true;
|
|
585
|
+
|
|
586
|
+
for (const child of children) {
|
|
587
|
+
if (!child) continue;
|
|
588
|
+
|
|
589
|
+
if (child.type === 'space') {
|
|
590
|
+
if (!firstBlock) {
|
|
591
|
+
out += `${continuationPrefix}${EOL}`;
|
|
592
|
+
}
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (child.type === 'list') {
|
|
597
|
+
if (firstBlock) {
|
|
598
|
+
out += `${markerPrefix.trimEnd()}${EOL}`;
|
|
599
|
+
firstBlock = false;
|
|
600
|
+
}
|
|
601
|
+
out += formatToken(child, nestedListIndent, null, token, childWidth, depth + 1);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const rendered = formatToken(child, listBaseIndent, orderedListNumber, token, childWidth, depth);
|
|
606
|
+
const body = trimTrailingEol(rendered);
|
|
607
|
+
if (!body) continue;
|
|
608
|
+
|
|
609
|
+
if (firstBlock) {
|
|
610
|
+
out += `${prefixFirstAndRest(body, markerPrefix, continuationPrefix)}${EOL}`;
|
|
611
|
+
firstBlock = false;
|
|
612
|
+
} else {
|
|
613
|
+
out += `${prefixLines(body, continuationPrefix)}${EOL}`;
|
|
614
|
+
}
|
|
175
615
|
}
|
|
616
|
+
|
|
617
|
+
if (firstBlock) return `${markerPrefix.trimEnd()}${EOL}`;
|
|
618
|
+
return out;
|
|
176
619
|
}
|
|
177
620
|
|
|
178
621
|
/**
|
|
179
622
|
* Pad `content` to `targetWidth` per alignment. `displayWidth` is the visible
|
|
180
|
-
* width of `content` (ANSI codes excluded).
|
|
623
|
+
* width of `content` (ANSI codes excluded).
|
|
181
624
|
*/
|
|
182
625
|
export function padAligned(content, displayWidth, targetWidth, align) {
|
|
183
626
|
const padding = Math.max(0, targetWidth - displayWidth);
|