ANSI escapes working in some semblence
This commit is contained in:
parent
a698136493
commit
19cf7246e8
8 changed files with 184 additions and 23 deletions
|
@ -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 Image from './types/image';
|
import ContentTypeError from './types/error/content_type';
|
||||||
import Text from './types/text';
|
import Image from './types/image';
|
||||||
|
import Terminal from './types/terminal';
|
||||||
|
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} />);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default function Error({ content_type }: { content_type: string }) {
|
|
||||||
return <p>{`Unrecognised MIME type: ${content_type}`}</p>
|
|
||||||
}
|
|
3
src/app/[...file]/types/error/content_type.tsx
Normal file
3
src/app/[...file]/types/error/content_type.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function ContentTypeError({ content_type }: { content_type: string }) {
|
||||||
|
return <p>{`Unrecognised MIME type: ${content_type}`}</p>
|
||||||
|
}
|
3
src/app/[...file]/types/error/network.tsx
Normal file
3
src/app/[...file]/types/error/network.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function NetworkError({ err }: { err: string }) {
|
||||||
|
return <p>{`Error: ${err}`}</p>;
|
||||||
|
}
|
13
src/app/[...file]/types/terminal.css.ts
Normal file
13
src/app/[...file]/types/terminal.css.ts
Normal 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,
|
||||||
|
});
|
66
src/app/[...file]/types/terminal.tsx
Normal file
66
src/app/[...file]/types/terminal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue