ProzillaOS/packages/apps/terminal/components/Ansi.tsx
2024-06-16 21:46:44 +02:00

174 lines
4.5 KiB
TypeScript

import Anser, { AnserJsonEntry } from "anser";
import { escapeCarriageReturn } from "escape-carriage";
import * as React from "react";
import styles from "./Terminal.module.css";
/**
* Source:
* https://github.com/nteract/ansi-to-react/blob/master/src/index.ts
*/
/**
* Converts ANSI strings into JSON output.
* @param input - The input string.
* @param use_classes - If `true`, HTML classes will be appended
* to the HTML output.
* @returns The parsed input.
*/
function ansiToJSON(input: string, use_classes: boolean | undefined): AnserJsonEntry[] {
input = escapeCarriageReturn(fixBackspace(input));
return Anser.ansiToJson(input, {
json: true,
remove_empty: true,
use_classes,
});
}
/**
* Create a class string.
* @returns class name(s)
*/
function createClass(bundle: AnserJsonEntry): string | null {
const classNames = [];
if (bundle.bg) {
classNames.push(styles[`${bundle.bg}-bg`]);
}
if (bundle.fg) {
classNames.push(styles[`${bundle.fg}-fg`]);
}
if (bundle.decoration) {
classNames.push(styles[`ansi-${bundle.decoration}`]);
}
if (classNames.length === 0) {
return null;
}
return classNames.join(" ");
}
/**
* Create the style attribute.
* @param bundle
* @returns returns the style object
*/
function createStyle(bundle: AnserJsonEntry): object {
const style: Record<string, string> = {};
if (bundle.bg) {
style.backgroundColor = `rgb(${bundle.bg})`;
}
if (bundle.fg) {
style.color = `rgb(${bundle.fg})`;
}
switch (bundle.decoration) {
case "bold":
style.fontWeight = "bold";
break;
case "dim":
style.opacity = "0.5";
break;
case "italic":
style.fontStyle = "italic";
break;
case "hidden":
style.visibility = "hidden";
break;
case "strikethrough":
style.textDecoration = "line-through";
break;
case "underline":
style.textDecoration = "underline";
break;
case "blink":
style.textDecoration = "blink";
break;
default:
break;
}
return style;
}
/**
* Converts an Anser bundle into a React Node.
* @param linkify - whether links should be converting into clickable anchor tags.
* @param useClasses - should render the span with a class instead of style.
* @param bundle - Anser output.
*/
function convertBundleIntoReact(linkify: boolean, useClasses: boolean, bundle: AnserJsonEntry, key: number) {
const style = useClasses ? null : createStyle(bundle);
const className = useClasses ? createClass(bundle) : null;
if (!linkify) {
return React.createElement(
"pre",
{ style, key, className },
bundle.content
);
}
const content = [];
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
let index = 0;
let match: RegExpExecArray | null;
while ((match = linkRegex.exec(bundle.content)) !== null) {
const [, pre, url] = match;
const startIndex = match.index + pre.length;
if (startIndex > index) {
content.push(bundle.content.substring(index, startIndex));
}
// Make sure the href we generate from the link is fully qualified. We assume http
// if it starts with a www because many sites don't support https
const href = url.startsWith("www.") ? `http://${url}` : url;
content.push(
React.createElement(
"a",
{
key: index,
href,
target: "_blank",
},
`${url}`
)
);
index = linkRegex.lastIndex;
}
if (index < bundle.content.length) {
content.push(bundle.content.substring(index));
}
return React.createElement("span", { style, key, className }, content);
}
export function Ansi(props: { children?: string | undefined; linkify?: boolean | undefined; className?: string | undefined; useClasses?: boolean | undefined; }) {
const { className, useClasses, children, linkify } = props;
return React.createElement(
"code",
{ className },
ansiToJSON(children ?? "", useClasses ?? false).map(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
)
);
}
// This is copied from the Jupyter Classic source code
// notebook/static/base/js/utils.js to handle \b in a way
// that is **compatible with Jupyter classic**. One can
// argue that this behavior is questionable:
// https://stackoverflow.com/questions/55440152/multiple-b-doesnt-work-as-expected-in-jupyter#
function fixBackspace(txt: string) {
let tmp = txt;
do {
txt = tmp;
// Cancel out anything-but-newline followed by backspace
// eslint-disable-next-line no-control-regex
tmp = txt.replace(/[^\n]\x08/gm, "");
} while (tmp.length < txt.length);
return txt;
}