132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import Anser, { type AnserJsonEntry } from "anser";
|
|
import { escapeCarriageReturn } from "escape-carriage";
|
|
import fs from "node:fs";
|
|
import type { Plugin } from "vite";
|
|
|
|
const colorMap: Record<string, string> = {
|
|
"ansi-black": "color: var(--black)",
|
|
"ansi-red": "color: var(--red)",
|
|
"ansi-green": "color: var(--green)",
|
|
"ansi-yellow": "color: var(--brown)",
|
|
"ansi-blue": "color: var(--blue)",
|
|
"ansi-magenta": "color: var(--magenta)",
|
|
"ansi-cyan": "color: var(--cyan)",
|
|
"ansi-white": "color: var(--light-gray)",
|
|
"ansi-bright-black": "color: var(--dark-gray)",
|
|
"ansi-bright-red": "color: var(--light-red)",
|
|
"ansi-bright-green": "color: var(--light-green)",
|
|
"ansi-bright-yellow": "color: var(--yellow)",
|
|
"ansi-bright-blue": "color: var(--light-blue)",
|
|
"ansi-bright-magenta": "color: var(--light-magenta)",
|
|
"ansi-bright-cyan": "color: var(--light-cyan)",
|
|
"ansi-bright-white": "color: var(--white)",
|
|
};
|
|
|
|
const bgColorMap: Record<string, string> = {
|
|
"ansi-black": "background-color: transparent",
|
|
"ansi-red": "background-color: var(--red)",
|
|
"ansi-green": "background-color: var(--green)",
|
|
"ansi-yellow": "background-color: var(--brown)",
|
|
"ansi-blue": "background-color: var(--blue)",
|
|
"ansi-magenta": "background-color: var(--magenta)",
|
|
"ansi-cyan": "background-color: var(--cyan)",
|
|
"ansi-white": "background-color: var(--light-gray)",
|
|
"ansi-bright-black": "background-color: var(--dark-gray)",
|
|
"ansi-bright-red": "background-color: var(--light-red)",
|
|
"ansi-bright-green": "background-color: var(--light-green)",
|
|
"ansi-bright-yellow": "background-color: var(--yellow)",
|
|
"ansi-bright-blue": "background-color: var(--light-blue)",
|
|
"ansi-bright-magenta": "background-color: var(--light-magenta)",
|
|
"ansi-bright-cyan": "background-color: var(--light-cyan)",
|
|
"ansi-bright-white": "background-color: var(--white)",
|
|
};
|
|
|
|
const decorationMap: Record<string, string> = {
|
|
bold: "font-weight: 700",
|
|
dim: "opacity: 0.5",
|
|
italic: "font-style: italic",
|
|
hidden: "visibility: hidden",
|
|
strikethrough: "text-decoration: line-through",
|
|
underline: "text-decoration: underline",
|
|
blink: "animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
|
};
|
|
|
|
function fixBackspace(txt: string): string {
|
|
let tmp = txt;
|
|
do {
|
|
txt = tmp;
|
|
tmp = txt.replace(/[^\n]\x08/gm, "");
|
|
} while (tmp.length < txt.length);
|
|
return txt;
|
|
}
|
|
|
|
function createStyle(bundle: AnserJsonEntry): string | null {
|
|
const declarations: string[] = [];
|
|
if (bundle.bg && bgColorMap[bundle.bg]) {
|
|
declarations.push(bgColorMap[bundle.bg]!);
|
|
}
|
|
if (bundle.fg && colorMap[bundle.fg]) {
|
|
declarations.push(colorMap[bundle.fg]!);
|
|
}
|
|
if (bundle.decoration && decorationMap[bundle.decoration]) {
|
|
declarations.push(decorationMap[bundle.decoration]!);
|
|
}
|
|
return declarations.length ? declarations.join("; ") : null;
|
|
}
|
|
|
|
function escapeHtml(str: string): string {
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function renderAnsiToHtml(raw: string): string {
|
|
const input = escapeCarriageReturn(fixBackspace(raw));
|
|
const bundles = Anser.ansiToJson(input, {
|
|
json: true,
|
|
remove_empty: true,
|
|
use_classes: true,
|
|
});
|
|
|
|
const spans = bundles
|
|
.map((bundle) => {
|
|
const style = createStyle(bundle);
|
|
const content = escapeHtml(bundle.content);
|
|
if (style) {
|
|
return `<span style="${style}">${content}</span>`;
|
|
}
|
|
return `<span>${content}</span>`;
|
|
})
|
|
.join("");
|
|
|
|
return `<div class="flex justify-center"><pre style="text-align:left"><code>${spans}</code></pre></div>`;
|
|
}
|
|
|
|
const ANSI_SUFFIX = "?ansi";
|
|
|
|
export default function ansiPlugin(): Plugin {
|
|
return {
|
|
name: "vite-plugin-ansi",
|
|
enforce: "pre",
|
|
async resolveId(source, importer, options) {
|
|
if (!source.endsWith(ANSI_SUFFIX)) return;
|
|
const bare = source.slice(0, -ANSI_SUFFIX.length);
|
|
const resolved = await this.resolve(bare, importer, {
|
|
...options,
|
|
skipSelf: true,
|
|
});
|
|
if (resolved) {
|
|
return resolved.id + ANSI_SUFFIX;
|
|
}
|
|
},
|
|
load(id) {
|
|
if (!id.endsWith(ANSI_SUFFIX)) return;
|
|
const filePath = id.slice(0, -ANSI_SUFFIX.length);
|
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
const html = renderAnsiToHtml(raw);
|
|
return `export default ${JSON.stringify(html)};`;
|
|
},
|
|
};
|
|
}
|