ANSI escapes working in some semblence

This commit is contained in:
Gnarwhal 2024-09-17 07:11:54 +00:00
parent a698136493
commit 19cf7246e8
Signed by: Gnarwhal
GPG key ID: 0989A73D8C421174
8 changed files with 184 additions and 23 deletions

View file

@ -2,29 +2,58 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import Error from './types/error'; import NetworkError from './types/error/network';
import ContentTypeError from './types/error/content_type';
import Image from './types/image'; import Image from './types/image';
import Terminal from './types/terminal';
import Text from './types/text'; import Text from './types/text';
type ContentType<T> = {
content_type: RegExp,
extension: RegExp,
emit: () => undefined | Processor<T>,
};
type Processor<T> = {
process: (response: Response) => Promise<T>,
postprocess: (data: T) => undefined,
};
function not_match(regex: RegExp | undefined, str: string) {
return !(regex ?? /(?:)/).test(str);
}
function is_type(response: Response, type: ContentType<Any>) {
if (not_match(type.content_type, response.headers.get('Content-Type').split(';')[0])) {
return false;
} else if (not_match(type.path, window.location.pathname)) {
return false;
}
return true;
}
export default function Content({ src }: { src: string}) { export default function Content({ src }: { src: string}) {
const [content, set_content] = useState(); const [content, set_content] = useState();
type ContentType<T> = {
matcher: RegExp,
emit: () => undefined | Processor<T>,
};
type Processor<T> = {
process: (response: Response) => Promise<T>,
postprocess: (data: T) => undefined,
};
const recognized_types: ContentType<Any>[] = [{ const recognized_types: ContentType<Any>[] = [{
matcher: /image\/\w+/, content_type: /image\/\w+/,
emit: () => { emit: () => {
set_content(<Image src={src} />); set_content(<Image src={src} />);
}, },
}, { }, {
matcher: /text\/\w+/, content_type: /application\/octet-stream/,
path: /.*\.term/,
emit: () => {
return {
process: (response: Response) => {
return response.text();
},
postprocess: (data: string) => {
set_content(<Terminal text={data} />);
}
};
}
}, {
content_type: /(text\/\w+)|(application\/octet-stream)/,
emit: () => { emit: () => {
return { return {
process: (response: Response) => { process: (response: Response) => {
@ -42,8 +71,8 @@ export default function Content({ src }: { src: string}) {
const result = fetch(src) const result = fetch(src)
.then(response => { .then(response => {
const content_type = response.headers.get('Content-Type').split(';')[0]; const content_type = response.headers.get('Content-Type').split(';')[0];
for (let type of recognized_types) { for (const type of recognized_types) {
if (type.matcher.test(content_type)) { if (type.content_type.test(content_type)) {
const emitted = type.emit(); const emitted = type.emit();
if (emitted != undefined) { if (emitted != undefined) {
result.then(emitted.postprocess); result.then(emitted.postprocess);
@ -52,7 +81,10 @@ export default function Content({ src }: { src: string}) {
return; return;
} }
} }
set_content(<Error content_type={content_type} />); set_content(<ContentTypeError content_type={content_type} />);
})
.catch(err => {
set_content(<NetworkError err={err} />);
}); });
} }
}, []); }, []);

View file

@ -1,3 +0,0 @@
export default function Error({ content_type }: { content_type: string }) {
return <p>{`Unrecognised MIME type: ${content_type}`}</p>
}

View file

@ -0,0 +1,3 @@
export default function ContentTypeError({ content_type }: { content_type: string }) {
return <p>{`Unrecognised MIME type: ${content_type}`}</p>
}

View file

@ -0,0 +1,3 @@
export default function NetworkError({ err }: { err: string }) {
return <p>{`Error: ${err}`}</p>;
}

View file

@ -0,0 +1,13 @@
import { style } from '@vanilla-extract/css'
import * as colors from '../../colors.css'
export const content = style({
margin: 0,
fontFamily: 'monospace',
fontSize: '1.1em',
lineHeight: '1.4em',
whiteSpace: 'pre-wrap',
tabSize: 4,
});

View file

@ -0,0 +1,66 @@
import * as style from './terminal.css';
const ansi_normal_colors = [
"#000000",
"#bc2232",
"#2e9662",
"#a55d14",
"#2f5dab",
"#694Caa",
"#006080",
"#a5a2af",
];
const ansi_bright_colors = [
"#44444f",
"#f22c40",
"#39bb7a",
"#d6781a",
"#407ee7",
"#8466ea",
"#0082ad",
"#FFFFFF",
];
function ansi(text: string) {
const segments = text.split(/(?=\033)/g);
const spans = [];
const style = {
color: undefined,
backgroundColor: undefined,
fontWeight: undefined,
}
for (const [index, segment] of segments.entries()) {
const ansi_segment = segment.match(/\033\[(\d+;)*(\d+)m/g)[0]
console.log(ansi_segment);
const escape_codes = [
...
ansi_segment.matchAll(/\d+/g)
].map(element => element[0]);
for (const code of escape_codes) {
if (code == 0) {
style.color = undefined;
style.backgroundColor = undefined;
style.fontWeight = undefined;
} else if (code == 1) {
style.fontWeight = 'bold';
} else if (30 <= code && code <= 37) {
style.color = ansi_normal_colors[code - 30];
} else if (90 <= code && code <= 97) {
style.color = ansi_bright_colors[code - 90];
} else if (40 <= code && code <= 47) {
style.backgroundColor = ansi_bright_colors[code - 40];
} else if (100 <= code && code <= 107) {
style.backgroundColor = ansi_bright_colors[code - 100];
}
}
spans.push(<span key={index} style={{...style}}>{segment.substring(ansi_segment.length)}</span>);
}
return spans;
}
export default function Terminal({ text }: { text: string }) {
return (
<p className={style.content}>{ansi(text)}</p>
);
}

View file

@ -1,6 +1,27 @@
import { style } from '@vanilla-extract/css' import { style } from '@vanilla-extract/css'
export const text = style({ import * as colors from '../../colors.css'
export const group = style({
display: 'flex',
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: '1.1em',
lineHeight: '1.4em',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
tabSize: 4,
});
export const line_numbers = style({
margin: 0,
marginRight: '0.5em',
borderRight: `1px solid ${colors.background2}`,
paddingRight: '0.5em',
color: colors.foreground2,
});
export const content = style({
});
export const text = style({
margin: 0,
}); });

View file

@ -1,7 +1,33 @@
import * as style from './text.css' import { useLayoutEffect, useRef } from 'react';
import * as style from './text.css';
export default function Text({ text }: { text: string }) { export default function Text({ text }: { text: string }) {
const lines = text.split('\n');
const refs = lines.map(() => useRef(null));
useLayoutEffect(() => {
for (let line of lines) {
}
}, []);
return ( return (
<p className={style.text}>{text}</p> <div className={style.group}>
<div className={style.line_numbers}>
{lines.map((_, index) => {
return <p key={index} className={style.text}>{index}</p>;
})}
</div>
<div className={style.content}>
{lines.map((line, index) => {
if (line != '') {
return <p ref={refs[index]} key={index} className={style.text}>{line}</p>;
} else {
return <p ref={refs[index]} key={index} className={style.text}>{'\n'}</p>;
}
})}
</div>
</div>
); );
} }