Add syntax highlighting to markdown code blocks

This commit is contained in:
Sergii Kozak 2025-12-17 20:15:44 -08:00
parent 4ec2708bb3
commit f8e6d62db7
4 changed files with 313 additions and 11 deletions

View file

@ -44,6 +44,7 @@
"@mariozechner/pi-ai": "^0.23.3",
"@mariozechner/pi-tui": "^0.23.3",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",
"file-type": "^21.1.1",
"glob": "^11.0.3",

View file

@ -1,4 +1,5 @@
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as path from "node:path";
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
@ -630,6 +631,50 @@ export function stopThemeWatcher(): void {
// TUI Helpers
// ============================================================================
type CliHighlightTheme = Record<string, (s: string) => string>;
let cachedHighlightThemeFor: Theme | undefined;
let cachedCliHighlightTheme: CliHighlightTheme | undefined;
function buildCliHighlightTheme(t: Theme): CliHighlightTheme {
return {
keyword: (s: string) => t.fg("syntaxKeyword", s),
built_in: (s: string) => t.fg("syntaxType", s),
literal: (s: string) => t.fg("syntaxNumber", s),
number: (s: string) => t.fg("syntaxNumber", s),
string: (s: string) => t.fg("syntaxString", s),
comment: (s: string) => t.fg("syntaxComment", s),
function: (s: string) => t.fg("syntaxFunction", s),
title: (s: string) => t.fg("syntaxFunction", s),
class: (s: string) => t.fg("syntaxType", s),
type: (s: string) => t.fg("syntaxType", s),
attr: (s: string) => t.fg("syntaxVariable", s),
variable: (s: string) => t.fg("syntaxVariable", s),
params: (s: string) => t.fg("syntaxVariable", s),
operator: (s: string) => t.fg("syntaxOperator", s),
punctuation: (s: string) => t.fg("syntaxPunctuation", s),
};
}
function getCliHighlightTheme(t: Theme): CliHighlightTheme {
if (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {
cachedHighlightThemeFor = t;
cachedCliHighlightTheme = buildCliHighlightTheme(t);
}
return cachedCliHighlightTheme;
}
function requireCliHighlight(): { highlight: (code: string, opts?: any) => string } {
try {
const require = createRequire(import.meta.url);
return require("cli-highlight");
} catch {
return {
highlight: (code: string) => code,
};
}
}
export function getMarkdownTheme(): MarkdownTheme {
return {
heading: (text: string) => theme.fg("mdHeading", text),
@ -646,6 +691,19 @@ export function getMarkdownTheme(): MarkdownTheme {
italic: (text: string) => theme.italic(text),
underline: (text: string) => theme.underline(text),
strikethrough: (text: string) => chalk.strikethrough(text),
highlightCode: (code: string, lang?: string): string[] => {
const { highlight } = requireCliHighlight();
const opts: any = {
language: lang,
ignoreIllegals: true,
theme: getCliHighlightTheme(theme),
};
try {
return highlight(code, opts).split("\n");
} catch {
return code.split("\n").map((line) => theme.fg("mdCodeBlock", line));
}
},
};
}