mixdog 0.9.0 → 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/package.json +3 -3
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/src/headless-role.mjs +1 -1
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +2 -2
- package/src/mixdog-session-runtime.mjs +0 -1
- package/src/repl.mjs +0 -2
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +27 -49
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +8 -27
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +52 -182
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -30
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +258 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/channels/index.mjs +0 -30
- 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/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +1 -10
- 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 +3 -24
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +9 -7
- package/src/runtime/search/index.mjs +2 -7
- 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 +0 -1
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/standalone/seeds.mjs +1 -11
- package/src/tui/App.jsx +35 -12
- package/src/tui/components/PromptInput.jsx +63 -2
- package/src/tui/components/ToolExecution.jsx +7 -2
- package/src/tui/components/tool-output-format.mjs +156 -22
- package/src/tui/components/tool-output-format.test.mjs +93 -1
- package/src/tui/dist/index.mjs +473 -116
- package/src/tui/markdown/format-token.mjs +267 -108
- package/src/tui/markdown/format-token.test.mjs +105 -9
- package/src/tui/theme.mjs +10 -0
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/vendor/ink/build/ink.js +54 -8
- 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/runtime/channels/lib/hook-pipe-server.mjs +0 -671
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
* - chalk is forced to truecolor (level 3) so colors render regardless of the
|
|
6
6
|
* ambient TTY detection (we control the surface).
|
|
7
7
|
* - The `permission`/code accent and blockquote bar come from our theme.mjs.
|
|
8
|
-
* - `code` (fenced)
|
|
9
|
-
*
|
|
8
|
+
* - `code` (fenced) uses cli-highlight + theme syntax palette; `codespan`
|
|
9
|
+
* (inline) gets the accent color.
|
|
10
10
|
* - `table` is NOT handled here — the React component (MarkdownTable.jsx)
|
|
11
11
|
* renders tables with proper ink Box layout (hybrid split).
|
|
12
12
|
* - Hyperlinks/issue-ref linkify are dropped (no OSC-8 dependency); link text
|
|
13
13
|
* is shown plainly with its URL.
|
|
14
14
|
*/
|
|
15
15
|
import { Chalk } from 'chalk';
|
|
16
|
+
import { highlight, supportsLanguage } from 'cli-highlight';
|
|
16
17
|
import stripAnsi from 'strip-ansi';
|
|
17
18
|
import stringWidth from 'string-width';
|
|
18
19
|
import wrapAnsi from 'wrap-ansi';
|
|
@@ -101,6 +102,7 @@ export function extraColorizers() {
|
|
|
101
102
|
body: fallbackBody,
|
|
102
103
|
// Truecolor background band for fenced code blocks (per-line wrap).
|
|
103
104
|
codeBg: rgbBg(theme.mdCodeBlockBg ?? theme.background),
|
|
105
|
+
codeSpanBg: rgbBg(theme.mdCodeSpanBg ?? theme.mdCodeBlockBg ?? theme.background),
|
|
104
106
|
};
|
|
105
107
|
return _extra;
|
|
106
108
|
}
|
|
@@ -124,12 +126,10 @@ function decodeEntities(s) {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// ── Fenced code block rendering ─────────────────────────────────────────────
|
|
127
|
-
//
|
|
128
|
-
// Diff/patch languages
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// languages fall back to the flat `mdCodeBlock` body color.
|
|
132
|
-
const CODE_BLOCK_INDENT = ' ';
|
|
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
133
|
const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
|
|
134
134
|
|
|
135
135
|
/** Wrap text to width, ANSI-aware (lockstep with table-layout hard wrap). */
|
|
@@ -167,17 +167,36 @@ function hardWrapAnsiLines(text, width) {
|
|
|
167
167
|
return out.length > 0 ? out : [''];
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
/** Wrap one logical code line (ANSI content
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const segments = hardWrapAnsiLines(ansiContent, contentMax);
|
|
175
|
-
return segments.map((seg) => `${CODE_BLOCK_INDENT}${seg}`);
|
|
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);
|
|
176
174
|
}
|
|
177
175
|
|
|
178
|
-
/**
|
|
179
|
-
function
|
|
180
|
-
return
|
|
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 };
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
/** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
|
|
@@ -266,34 +285,60 @@ function colorizeDiffStatTrailer(line, c) {
|
|
|
266
285
|
.replace(/(\d+)(\s+deletions?\(-\))/g, (_, n, rest) => `${c.diffRemoved(n)}${c.diffContext(rest)}`);
|
|
267
286
|
}
|
|
268
287
|
|
|
269
|
-
function
|
|
288
|
+
function collectDiffBodyLines(text, c, bandWidth) {
|
|
270
289
|
const lines = [];
|
|
271
290
|
for (const line of String(text ?? '').split(EOL)) {
|
|
272
291
|
const colored = colorizeDiffLine(line, c);
|
|
273
|
-
lines.push(...
|
|
292
|
+
lines.push(...wrapCodeLine(colored, bandWidth));
|
|
274
293
|
}
|
|
275
|
-
return
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ──
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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',
|
|
292
337
|
};
|
|
293
338
|
|
|
294
339
|
export const LANG_FAMILY = {
|
|
295
340
|
js: 'js', javascript: 'js', mjs: 'js', cjs: 'js',
|
|
296
|
-
ts: '
|
|
341
|
+
ts: 'ts', typescript: 'ts', jsx: 'jsx', tsx: 'tsx',
|
|
297
342
|
json: 'json', json5: 'json',
|
|
298
343
|
bash: 'sh', sh: 'sh', shell: 'sh', zsh: 'sh',
|
|
299
344
|
python: 'py', py: 'py',
|
|
@@ -308,64 +353,187 @@ export const LANG_FAMILY = {
|
|
|
308
353
|
sql: 'sql',
|
|
309
354
|
yaml: 'yaml', yml: 'yaml',
|
|
310
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',
|
|
311
377
|
};
|
|
312
378
|
|
|
313
|
-
|
|
314
|
-
const
|
|
379
|
+
const HIGHLIGHT_CACHE_MAX = 300;
|
|
380
|
+
const highlightCache = new Map();
|
|
315
381
|
|
|
316
|
-
/**
|
|
317
|
-
export function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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);
|
|
350
482
|
}
|
|
351
|
-
|
|
483
|
+
highlightCache.set(cacheKey, out);
|
|
484
|
+
return out;
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
352
487
|
}
|
|
353
|
-
if (last < line.length) out += c.body(line.slice(last));
|
|
354
|
-
return out;
|
|
355
488
|
}
|
|
356
489
|
|
|
357
|
-
|
|
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) {
|
|
358
513
|
const lines = [];
|
|
359
514
|
for (const line of String(text ?? '').split(EOL)) {
|
|
360
|
-
const colored =
|
|
361
|
-
lines.push(...
|
|
515
|
+
const colored = codeBlock(line);
|
|
516
|
+
lines.push(...wrapCodeLine(colored, bandWidth));
|
|
362
517
|
}
|
|
363
|
-
return
|
|
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;
|
|
364
532
|
}
|
|
365
533
|
|
|
366
534
|
/**
|
|
367
|
-
* Render a fenced `code` token:
|
|
368
|
-
* and language-aware coloring (no visible ``` fence lines).
|
|
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).
|
|
369
537
|
*/
|
|
370
538
|
function renderCodeBlock(token, width = 0) {
|
|
371
539
|
const { codeBlock } = colorizers();
|
|
@@ -374,30 +542,20 @@ function renderCodeBlock(token, width = 0) {
|
|
|
374
542
|
const text = decodeEntities(token.text ?? '');
|
|
375
543
|
const bandWidth = Math.max(8, Number(width) || 80);
|
|
376
544
|
|
|
377
|
-
let
|
|
545
|
+
let bodyLines;
|
|
378
546
|
if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
|
|
379
|
-
|
|
547
|
+
bodyLines = collectDiffBodyLines(text, c, bandWidth);
|
|
380
548
|
} else {
|
|
549
|
+
const hljsLang = resolveHljsLanguage(lang);
|
|
381
550
|
const family = LANG_FAMILY[lang];
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
// markdown highlighter); unknown langs fall back to the flat body color.
|
|
385
|
-
if (family && family !== 'md') {
|
|
386
|
-
body = renderHighlightedBody(text, family, c, bandWidth);
|
|
551
|
+
if (hljsLang && family !== 'md') {
|
|
552
|
+
bodyLines = collectHighlightedBodyLines(text, hljsLang, bandWidth);
|
|
387
553
|
} else {
|
|
388
|
-
|
|
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);
|
|
554
|
+
bodyLines = collectFlatBodyLines(text, codeBlock, bandWidth);
|
|
395
555
|
}
|
|
396
556
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
: '';
|
|
400
|
-
return `${langLine}${body}${body ? EOL : ''}`;
|
|
557
|
+
const { bodyOut } = tintCodeBlockBody(null, bodyLines, c.codeBg);
|
|
558
|
+
return `${bodyOut}${bodyOut ? EOL : ''}`;
|
|
401
559
|
}
|
|
402
560
|
|
|
403
561
|
function numberToLetter(n) {
|
|
@@ -455,19 +613,19 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
|
|
|
455
613
|
const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
|
|
456
614
|
return inner
|
|
457
615
|
.split(EOL)
|
|
458
|
-
.map((line) =>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
)
|
|
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
|
+
})
|
|
463
621
|
.join(EOL);
|
|
464
622
|
}
|
|
465
623
|
case 'code':
|
|
466
|
-
// Fenced block:
|
|
624
|
+
// Fenced block: flush-left wrapped body with syntax highlighting.
|
|
467
625
|
return renderCodeBlock(token, width);
|
|
468
626
|
case 'codespan':
|
|
469
627
|
// inline code
|
|
470
|
-
return accent(decodeEntities(token.text));
|
|
628
|
+
return ex.codeSpanBg(accent(decodeEntities(token.text)));
|
|
471
629
|
case 'em':
|
|
472
630
|
return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
|
|
473
631
|
case 'strong':
|
|
@@ -598,7 +756,8 @@ function formatListItem(token, listBaseIndent, orderedListNumber, parent, depth
|
|
|
598
756
|
out += `${markerPrefix.trimEnd()}${EOL}`;
|
|
599
757
|
firstBlock = false;
|
|
600
758
|
}
|
|
601
|
-
|
|
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);
|
|
602
761
|
continue;
|
|
603
762
|
}
|
|
604
763
|
|
|
@@ -3,7 +3,12 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { marked } from 'marked';
|
|
4
4
|
import stripAnsi from 'strip-ansi';
|
|
5
5
|
import stringWidth from 'string-width';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
formatToken,
|
|
8
|
+
highlightCodeBlockToLines,
|
|
9
|
+
extraColorizers,
|
|
10
|
+
_highlightCacheSizeForTests,
|
|
11
|
+
} from './format-token.mjs';
|
|
7
12
|
import { setThemeSetting, theme } from '../theme.mjs';
|
|
8
13
|
|
|
9
14
|
setThemeSetting('mixdog', { persist: false });
|
|
@@ -18,14 +23,13 @@ function lexFirst(md, type) {
|
|
|
18
23
|
return tokens.find((t) => t.type === type) ?? tokens[0];
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
test('fenced code block
|
|
26
|
+
test('fenced code block has no ``` fences and flush-left body', () => {
|
|
22
27
|
const token = lexFirst('```js\nconst x = 1;\n```\n', 'code');
|
|
23
28
|
const out = formatToken(token);
|
|
24
29
|
const plain = stripAnsi(out);
|
|
25
|
-
assert.ok(plain.includes('js'), 'language label visible');
|
|
26
30
|
assert.ok(!plain.includes('```'), 'no literal fence markers');
|
|
27
|
-
assert.ok(plain.includes('
|
|
28
|
-
assert.ok(
|
|
31
|
+
assert.ok(plain.includes('const x = 1;'), 'body is flush-left');
|
|
32
|
+
assert.ok(!plain.trimStart().startsWith('js'), 'no language label row');
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
test('short code line has no terminal-width trailing padding band', () => {
|
|
@@ -33,7 +37,17 @@ test('short code line has no terminal-width trailing padding band', () => {
|
|
|
33
37
|
const out = formatToken(token, 0, null, null, 60);
|
|
34
38
|
const bodyLine = stripAnsi(out).split('\n').find((l) => l.includes('short'));
|
|
35
39
|
assert.ok(bodyLine, 'body line present');
|
|
36
|
-
assert.equal(bodyLine, '
|
|
40
|
+
assert.equal(bodyLine, 'short', 'body line is only text');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('code block body lines share one compact background width', () => {
|
|
44
|
+
const token = lexFirst('```js\na\n\nlonger line\n```\n', 'code');
|
|
45
|
+
const out = formatToken(token, 0, null, null, 80);
|
|
46
|
+
const plainLines = stripAnsi(out).split('\n').filter((l) => l.length > 0);
|
|
47
|
+
const widths = plainLines.map((l) => stringWidth(l));
|
|
48
|
+
assert.ok(widths.length >= 3, 'body lines rendered');
|
|
49
|
+
assert.equal(new Set(widths).size, 1, 'every row uses the same content width');
|
|
50
|
+
assert.equal(Math.max(...widths), stringWidth('longer line'), 'width follows longest rendered line');
|
|
37
51
|
});
|
|
38
52
|
|
|
39
53
|
test('long code line wraps within requested body width', () => {
|
|
@@ -41,7 +55,7 @@ test('long code line wraps within requested body width', () => {
|
|
|
41
55
|
const token = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
|
|
42
56
|
const width = 20;
|
|
43
57
|
const out = formatToken(token, 0, null, null, width);
|
|
44
|
-
const bodyLines = stripAnsi(out).split('\n').filter((l) => l.
|
|
58
|
+
const bodyLines = stripAnsi(out).split('\n').filter((l) => l.includes('a'));
|
|
45
59
|
assert.ok(bodyLines.length > 1, 'long line is wrapped into multiple rows');
|
|
46
60
|
for (const line of bodyLines) {
|
|
47
61
|
assert.ok(line.length <= width, `wrapped line "${line}" fits width ${width}`);
|
|
@@ -83,6 +97,52 @@ test('blockquote fenced code block respects narrowed width and has no fences', (
|
|
|
83
97
|
}
|
|
84
98
|
});
|
|
85
99
|
|
|
100
|
+
test('blockquote fenced code with blank line keeps quote prefix on every code row', () => {
|
|
101
|
+
const codeTok = lexFirst('```js\na\n\nb\n```\n', 'code');
|
|
102
|
+
const quoteTok = { type: 'blockquote', tokens: [codeTok] };
|
|
103
|
+
const outerWidth = 40;
|
|
104
|
+
const out = formatToken(quoteTok, 0, null, null, outerWidth);
|
|
105
|
+
const plain = stripAnsi(out);
|
|
106
|
+
assert.ok(!plain.includes('```'), 'no literal fence markers');
|
|
107
|
+
const bar = '\u258e';
|
|
108
|
+
const visibleLines = plain.split('\n').filter((l) => stringWidth(l) > 0);
|
|
109
|
+
assert.ok(visibleLines.length >= 3, 'two body lines + blank padded row');
|
|
110
|
+
for (const line of visibleLines) {
|
|
111
|
+
assert.ok(line.startsWith(bar), `quote bar on every visible row: "${line}"`);
|
|
112
|
+
assert.ok(stringWidth(line) <= outerWidth, `line fits outer width: "${line}"`);
|
|
113
|
+
}
|
|
114
|
+
const blankPadded = visibleLines.find((l) => !l.includes('a') && !l.includes('b'));
|
|
115
|
+
assert.ok(blankPadded, 'background-padded blank code row is rendered');
|
|
116
|
+
assert.ok(blankPadded.startsWith(bar), 'blank padded code row keeps quote prefix');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('two-level nested list code block does not double-subtract list prefix width', () => {
|
|
120
|
+
const long = 'd'.repeat(40);
|
|
121
|
+
const codeTok = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
|
|
122
|
+
const innerList = {
|
|
123
|
+
type: 'list',
|
|
124
|
+
ordered: false,
|
|
125
|
+
items: [{ type: 'list_item', tokens: [codeTok] }],
|
|
126
|
+
};
|
|
127
|
+
const outerList = {
|
|
128
|
+
type: 'list',
|
|
129
|
+
ordered: false,
|
|
130
|
+
items: [{ type: 'list_item', tokens: [innerList] }],
|
|
131
|
+
};
|
|
132
|
+
const outerWidth = 30;
|
|
133
|
+
const singleLevelNested = formatToken(innerList, 2, null, null, outerWidth);
|
|
134
|
+
const twoLevelNested = formatToken(outerList, 0, null, null, outerWidth);
|
|
135
|
+
const codeLines = (plain) => plain.split('\n').filter((l) => l.includes('d'));
|
|
136
|
+
const singleLines = codeLines(stripAnsi(singleLevelNested));
|
|
137
|
+
const nestedLines = codeLines(stripAnsi(twoLevelNested));
|
|
138
|
+
assert.equal(
|
|
139
|
+
nestedLines.length,
|
|
140
|
+
singleLines.length,
|
|
141
|
+
'nested list code wraps like one-level nested list at the same outer width',
|
|
142
|
+
);
|
|
143
|
+
assert.ok(nestedLines.length > 1, 'code is wrapped under narrow width');
|
|
144
|
+
});
|
|
145
|
+
|
|
86
146
|
test('js highlighting colors keyword, string, number distinctly', () => {
|
|
87
147
|
const token = lexFirst('```js\nconst n = 42;\nlet s = "hi";\n```\n', 'code');
|
|
88
148
|
const out = formatToken(token);
|
|
@@ -91,6 +151,42 @@ test('js highlighting colors keyword, string, number distinctly', () => {
|
|
|
91
151
|
assert.ok(out.includes(rgbSgr(theme.syntaxString)), 'string colored');
|
|
92
152
|
});
|
|
93
153
|
|
|
154
|
+
test('highlight cache serves repeat block highlight without changing output', () => {
|
|
155
|
+
const c = extraColorizers();
|
|
156
|
+
const text = `const cacheProbe_${Date.now()} = 1;\nlet y = 2;\n`;
|
|
157
|
+
const sizeBefore = _highlightCacheSizeForTests();
|
|
158
|
+
const first = highlightCodeBlockToLines(text, 'js', c);
|
|
159
|
+
assert.equal(_highlightCacheSizeForTests(), sizeBefore + 1, 'first highlight populates cache');
|
|
160
|
+
const second = highlightCodeBlockToLines(text, 'js', c);
|
|
161
|
+
assert.deepEqual(second, first);
|
|
162
|
+
assert.equal(_highlightCacheSizeForTests(), sizeBefore + 1, 'cache hit does not grow entries');
|
|
163
|
+
assert.ok(first[0].includes('38;2;'), 'highlighted ANSI present');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('inline codespan emits truecolor background band', () => {
|
|
167
|
+
const tokens = marked.lexer('`hello`');
|
|
168
|
+
const codespan = tokens[0].tokens[0];
|
|
169
|
+
assert.equal(codespan.type, 'codespan');
|
|
170
|
+
const out = formatToken(codespan);
|
|
171
|
+
assert.ok(out.includes('48;2;'), 'background SGR present');
|
|
172
|
+
assert.ok(stripAnsi(out).includes('hello'));
|
|
173
|
+
assert.ok(out.includes(rgbSgr(theme.mdCode)), 'inline code accent');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('kotlin fence resolves and highlights', () => {
|
|
177
|
+
const token = lexFirst('```kotlin\nfun main() {}\n```\n', 'code');
|
|
178
|
+
const out = formatToken(token);
|
|
179
|
+
assert.ok(out.includes(rgbSgr(theme.syntaxKeyword)) || out.includes(rgbSgr(theme.mdCodeBlock)));
|
|
180
|
+
assert.ok(stripAnsi(out).includes('fun main'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('typescript fence uses typescript highlighter', () => {
|
|
184
|
+
const token = lexFirst('```ts\nconst x: number = 1;\n```\n', 'code');
|
|
185
|
+
const out = formatToken(token);
|
|
186
|
+
assert.ok(out.includes('38;2;'));
|
|
187
|
+
assert.ok(stripAnsi(out).includes('const x'));
|
|
188
|
+
});
|
|
189
|
+
|
|
94
190
|
test('diff fenced block colors add/remove/hunk/header separately', () => {
|
|
95
191
|
const diff = [
|
|
96
192
|
'```diff',
|
|
@@ -142,7 +238,7 @@ test('unknown language falls back to flat code block color', () => {
|
|
|
142
238
|
const token = lexFirst('```foobarlang\nsome text\n```\n', 'code');
|
|
143
239
|
const out = formatToken(token);
|
|
144
240
|
assert.ok(out.includes(rgbSgr(theme.mdCodeBlock)), 'flat code-block color used');
|
|
145
|
-
assert.ok(stripAnsi(out).includes('
|
|
241
|
+
assert.ok(stripAnsi(out).includes('some text'), 'body is flush-left');
|
|
146
242
|
});
|
|
147
243
|
|
|
148
244
|
test('inline link emits OSC 8 hyperlink with styled label (URL hidden)', () => {
|
|
@@ -200,7 +296,7 @@ test('ordered list markers follow nesting depth (1. / a. / i.)', () => {
|
|
|
200
296
|
|
|
201
297
|
test('every palette defines the full extended key set', () => {
|
|
202
298
|
const required = [
|
|
203
|
-
'mdCodeBlockBorder', 'mdCodeBlockBg', 'mdLink', 'mdLinkText', 'mdStrong', 'mdEmph',
|
|
299
|
+
'mdCodeBlockBorder', 'mdCodeBlockBg', 'mdCodeSpanBg', 'mdLink', 'mdLinkText', 'mdStrong', 'mdEmph',
|
|
204
300
|
'mdDiffAdded', 'mdDiffRemoved', 'mdDiffHunk', 'mdDiffHeader', 'mdDiffContext',
|
|
205
301
|
'mdDiffAddedBg', 'mdDiffRemovedBg',
|
|
206
302
|
'syntaxComment', 'syntaxKeyword', 'syntaxFunction', 'syntaxVariable',
|