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 (
-
+
);
}
-
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 ``;
+}
-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",