diff --git a/package-lock.json b/package-lock.json index f28d1a3..c325151 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,7 @@ "name": "website", "version": "0.1.0", "dependencies": { - "anser": "^2.3.5", "cgol": "file:./cgol/pkg", - "escape-carriage": "^1.3.1", "react": "^19.2.4", "react-dom": "^19.2.4" }, @@ -20,6 +18,8 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", + "anser": "^2.3.5", + "escape-carriage": "^1.3.1", "eslint": "^10", "prettier": "^3.8.1", "prettier-plugin-tailwindcss": "^0.7.2", @@ -2257,6 +2257,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", + "dev": true, "license": "MIT" }, "node_modules/balanced-match": { @@ -2496,6 +2497,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz", "integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { diff --git a/package.json b/package.json index bc37431..0b6b7c1 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "anser": "^2.3.5", "cgol": "file:./cgol/pkg", - "escape-carriage": "^1.3.1", "react": "^19.2.4", "react-dom": "^19.2.4" }, "devDependencies": { + "anser": "^2.3.5", + "escape-carriage": "^1.3.1", "@tailwindcss/vite": "^4.2.1", "@types/node": "^25.3.3", "@types/react": "^19.2.14", diff --git a/src/App.tsx b/src/App.tsx index 20f94a5..a7d8bb6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { BorderedBox } from "~/components/bordered-box"; import { FrostedBox } from "~/components/frosted-box"; import Header from "~/components/header"; import { CgolCanvas } from "~/components/cgol-canvas"; -import Jet from "~/assets/Jet.txt?raw"; +import Jet from "~/assets/Jet.txt?ansi"; export default function App() { return ( diff --git a/src/components/header.tsx b/src/components/header.tsx index 8ce9936..13d6491 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,6 +1,3 @@ -import React from "react"; -import Ansi from "./ansi"; - interface HeaderProps { content: string; className?: string; @@ -8,9 +5,9 @@ interface HeaderProps { export default function Header({ content, className }: HeaderProps) { return ( -
- {content} -
+
); } - diff --git a/src/global.d.ts b/src/global.d.ts index 7e7aa76..145ad9e 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -5,6 +5,11 @@ declare module "*.txt?raw" { export default content; } +declare module "*.txt?ansi" { + const content: string; + export default content; +} + declare module "*.utf8ans?raw" { const content: string; export default content; diff --git a/src/components/ansi.tsx b/vite-plugin-ansi.ts similarity index 60% rename from src/components/ansi.tsx rename to vite-plugin-ansi.ts index d3508a6..7bc854a 100644 --- a/src/components/ansi.tsx +++ b/vite-plugin-ansi.ts @@ -1,6 +1,7 @@ import Anser, { type AnserJsonEntry } from "anser"; import { escapeCarriageReturn } from "escape-carriage"; -import React, { memo, useMemo } from "react"; +import fs from "node:fs"; +import type { Plugin } from "vite"; const colorMap: Record = { "ansi-black": "text-[var(--black)]", @@ -19,7 +20,7 @@ const colorMap: Record = { "ansi-bright-magenta": "text-[var(--light-magenta)]", "ansi-bright-cyan": "text-[var(--light-cyan)]", "ansi-bright-white": "text-[var(--white)]", -} as const; +}; const bgColorMap: Record = { "ansi-black": "bg-transparent", @@ -38,7 +39,7 @@ const bgColorMap: Record = { "ansi-bright-magenta": "bg-[var(--light-magenta)]", "ansi-bright-cyan": "bg-[var(--light-cyan)]", "ansi-bright-white": "bg-[var(--white)]", -} as const; +}; const decorationMap: Record = { bold: "font-bold", @@ -48,7 +49,7 @@ const decorationMap: Record = { strikethrough: "line-through", underline: "underline", blink: "animate-pulse", -} as const; +}; function fixBackspace(txt: string): string { let tmp = txt; @@ -61,7 +62,6 @@ function fixBackspace(txt: string): string { function createClass(bundle: AnserJsonEntry): string | null { const classes: string[] = []; - if (bundle.bg && bgColorMap[bundle.bg]) { classes.push(bgColorMap[bundle.bg]!); } @@ -74,41 +74,59 @@ function createClass(bundle: AnserJsonEntry): string | null { return classes.length ? classes.join(" ") : null; } -interface Props { - children?: string; - className?: string; +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); } -const Ansi = memo(function Ansi({ className, children = "" }: Props) { - const bundles = useMemo(() => { - const input = escapeCarriageReturn(fixBackspace(children)); - return Anser.ansiToJson(input, { - json: true, - remove_empty: true, - use_classes: true, - }); - }, [children]); +function renderAnsiToHtml(raw: string): string { + const input = escapeCarriageReturn(fixBackspace(raw)); + const bundles = Anser.ansiToJson(input, { + json: true, + remove_empty: true, + use_classes: true, + }); - const renderedContent = useMemo( - () => - bundles.map((bundle, key) => { - const bundleClassName = createClass(bundle); - return ( - - {bundle.content} - - ); - }), - [bundles], - ); + const spans = bundles + .map((bundle) => { + const cls = createClass(bundle); + const content = escapeHtml(bundle.content); + if (cls) { + return `${content}`; + } + return `${content}`; + }) + .join(""); - return ( -
-
-        {renderedContent}
-      
-
- ); -}); + return `
${spans}
`; +} -export default Ansi; +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)};`; + }, + }; +} diff --git a/vite.config.ts b/vite.config.ts index 02ab571..3a7addf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,9 +3,10 @@ import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; +import ansi from "./vite-plugin-ansi"; export default defineConfig({ - plugins: [react(), tailwindcss(), wasm(), topLevelAwait()], + plugins: [ansi(), react(), tailwindcss(), wasm(), topLevelAwait()], resolve: { alias: { "~": "/src",