feat: statically render the jet ansi text
This commit is contained in:
parent
e6a9b1a111
commit
5cbe032c23
7 changed files with 74 additions and 51 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -8,9 +8,7 @@
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anser": "^2.3.5",
|
|
||||||
"cgol": "file:./cgol/pkg",
|
"cgol": "file:./cgol/pkg",
|
||||||
"escape-carriage": "^1.3.1",
|
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
},
|
},
|
||||||
|
|
@ -20,6 +18,8 @@
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"anser": "^2.3.5",
|
||||||
|
"escape-carriage": "^1.3.1",
|
||||||
"eslint": "^10",
|
"eslint": "^10",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
|
|
@ -2257,6 +2257,7 @@
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz",
|
||||||
"integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==",
|
"integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
|
|
@ -2496,6 +2497,7 @@
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
|
||||||
"integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
|
"integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anser": "^2.3.5",
|
|
||||||
"cgol": "file:./cgol/pkg",
|
"cgol": "file:./cgol/pkg",
|
||||||
"escape-carriage": "^1.3.1",
|
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"anser": "^2.3.5",
|
||||||
|
"escape-carriage": "^1.3.1",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { BorderedBox } from "~/components/bordered-box";
|
||||||
import { FrostedBox } from "~/components/frosted-box";
|
import { FrostedBox } from "~/components/frosted-box";
|
||||||
import Header from "~/components/header";
|
import Header from "~/components/header";
|
||||||
import { CgolCanvas } from "~/components/cgol-canvas";
|
import { CgolCanvas } from "~/components/cgol-canvas";
|
||||||
import Jet from "~/assets/Jet.txt?raw";
|
import Jet from "~/assets/Jet.txt?ansi";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
import Ansi from "./ansi";
|
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
content: string;
|
content: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -8,9 +5,9 @@ interface HeaderProps {
|
||||||
|
|
||||||
export default function Header({ content, className }: HeaderProps) {
|
export default function Header({ content, className }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div
|
||||||
<Ansi>{content}</Ansi>
|
className={className}
|
||||||
</div>
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
5
src/global.d.ts
vendored
5
src/global.d.ts
vendored
|
|
@ -5,6 +5,11 @@ declare module "*.txt?raw" {
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "*.txt?ansi" {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "*.utf8ans?raw" {
|
declare module "*.utf8ans?raw" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import Anser, { type AnserJsonEntry } from "anser";
|
import Anser, { type AnserJsonEntry } from "anser";
|
||||||
import { escapeCarriageReturn } from "escape-carriage";
|
import { escapeCarriageReturn } from "escape-carriage";
|
||||||
import React, { memo, useMemo } from "react";
|
import fs from "node:fs";
|
||||||
|
import type { Plugin } from "vite";
|
||||||
|
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
"ansi-black": "text-[var(--black)]",
|
"ansi-black": "text-[var(--black)]",
|
||||||
|
|
@ -19,7 +20,7 @@ const colorMap: Record<string, string> = {
|
||||||
"ansi-bright-magenta": "text-[var(--light-magenta)]",
|
"ansi-bright-magenta": "text-[var(--light-magenta)]",
|
||||||
"ansi-bright-cyan": "text-[var(--light-cyan)]",
|
"ansi-bright-cyan": "text-[var(--light-cyan)]",
|
||||||
"ansi-bright-white": "text-[var(--white)]",
|
"ansi-bright-white": "text-[var(--white)]",
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
const bgColorMap: Record<string, string> = {
|
const bgColorMap: Record<string, string> = {
|
||||||
"ansi-black": "bg-transparent",
|
"ansi-black": "bg-transparent",
|
||||||
|
|
@ -38,7 +39,7 @@ const bgColorMap: Record<string, string> = {
|
||||||
"ansi-bright-magenta": "bg-[var(--light-magenta)]",
|
"ansi-bright-magenta": "bg-[var(--light-magenta)]",
|
||||||
"ansi-bright-cyan": "bg-[var(--light-cyan)]",
|
"ansi-bright-cyan": "bg-[var(--light-cyan)]",
|
||||||
"ansi-bright-white": "bg-[var(--white)]",
|
"ansi-bright-white": "bg-[var(--white)]",
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
const decorationMap: Record<string, string> = {
|
const decorationMap: Record<string, string> = {
|
||||||
bold: "font-bold",
|
bold: "font-bold",
|
||||||
|
|
@ -48,7 +49,7 @@ const decorationMap: Record<string, string> = {
|
||||||
strikethrough: "line-through",
|
strikethrough: "line-through",
|
||||||
underline: "underline",
|
underline: "underline",
|
||||||
blink: "animate-pulse",
|
blink: "animate-pulse",
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
function fixBackspace(txt: string): string {
|
function fixBackspace(txt: string): string {
|
||||||
let tmp = txt;
|
let tmp = txt;
|
||||||
|
|
@ -61,7 +62,6 @@ function fixBackspace(txt: string): string {
|
||||||
|
|
||||||
function createClass(bundle: AnserJsonEntry): string | null {
|
function createClass(bundle: AnserJsonEntry): string | null {
|
||||||
const classes: string[] = [];
|
const classes: string[] = [];
|
||||||
|
|
||||||
if (bundle.bg && bgColorMap[bundle.bg]) {
|
if (bundle.bg && bgColorMap[bundle.bg]) {
|
||||||
classes.push(bgColorMap[bundle.bg]!);
|
classes.push(bgColorMap[bundle.bg]!);
|
||||||
}
|
}
|
||||||
|
|
@ -74,41 +74,59 @@ function createClass(bundle: AnserJsonEntry): string | null {
|
||||||
return classes.length ? classes.join(" ") : null;
|
return classes.length ? classes.join(" ") : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
function escapeHtml(str: string): string {
|
||||||
children?: string;
|
return str
|
||||||
className?: string;
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Ansi = memo(function Ansi({ className, children = "" }: Props) {
|
function renderAnsiToHtml(raw: string): string {
|
||||||
const bundles = useMemo(() => {
|
const input = escapeCarriageReturn(fixBackspace(raw));
|
||||||
const input = escapeCarriageReturn(fixBackspace(children));
|
const bundles = Anser.ansiToJson(input, {
|
||||||
return Anser.ansiToJson(input, {
|
json: true,
|
||||||
json: true,
|
remove_empty: true,
|
||||||
remove_empty: true,
|
use_classes: true,
|
||||||
use_classes: true,
|
});
|
||||||
});
|
|
||||||
}, [children]);
|
|
||||||
|
|
||||||
const renderedContent = useMemo(
|
const spans = bundles
|
||||||
() =>
|
.map((bundle) => {
|
||||||
bundles.map((bundle, key) => {
|
const cls = createClass(bundle);
|
||||||
const bundleClassName = createClass(bundle);
|
const content = escapeHtml(bundle.content);
|
||||||
return (
|
if (cls) {
|
||||||
<span key={key} className={bundleClassName ?? undefined}>
|
return `<span class="${cls}">${content}</span>`;
|
||||||
{bundle.content}
|
}
|
||||||
</span>
|
return `<span>${content}</span>`;
|
||||||
);
|
})
|
||||||
}),
|
.join("");
|
||||||
[bundles],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return `<div class="flex justify-center"><pre style="text-align:left"><code>${spans}</code></pre></div>`;
|
||||||
<div className="flex justify-center">
|
}
|
||||||
<pre className={className ?? ""} style={{ textAlign: "left" }}>
|
|
||||||
<code>{renderedContent}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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)};`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,10 @@ import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import wasm from "vite-plugin-wasm";
|
import wasm from "vite-plugin-wasm";
|
||||||
import topLevelAwait from "vite-plugin-top-level-await";
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
|
import ansi from "./vite-plugin-ansi";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss(), wasm(), topLevelAwait()],
|
plugins: [ansi(), react(), tailwindcss(), wasm(), topLevelAwait()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"~": "/src",
|
"~": "/src",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue