@stainless-api/playgrounds 0.0.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +23 -0
- package/eslint.config.js +2 -0
- package/package.json +69 -0
- package/src/Logs.tsx +216 -0
- package/src/Panel.tsx +21 -0
- package/src/PlaygroundPanelWrapper.tsx +5 -0
- package/src/build-py-types.ts +152 -0
- package/src/build-ts-types.ts +70 -0
- package/src/build.ts +97 -0
- package/src/codemirror/comlink.ts +698 -0
- package/src/codemirror/curl/curlconverter.vendor.js +7959 -0
- package/src/codemirror/curl.ts +108 -0
- package/src/codemirror/deps.ts +12 -0
- package/src/codemirror/fix-lsp-markdown.ts +50 -0
- package/src/codemirror/lsp.ts +87 -0
- package/src/codemirror/python/anser.ts +398 -0
- package/src/codemirror/python/pyodide.ts +180 -0
- package/src/codemirror/python.ts +160 -0
- package/src/codemirror/react.tsx +615 -0
- package/src/codemirror/sanitize-html.ts +12 -0
- package/src/codemirror/shiki.ts +65 -0
- package/src/codemirror/typescript/cdn-typescript.d.ts +1 -0
- package/src/codemirror/typescript/cdn-typescript.js +1 -0
- package/src/codemirror/typescript/console.ts +590 -0
- package/src/codemirror/typescript/get-signature.ts +94 -0
- package/src/codemirror/typescript/prettier-plugin-external-typescript.vendor.js +4968 -0
- package/src/codemirror/typescript/runner.ts +396 -0
- package/src/codemirror/typescript/special-info.ts +171 -0
- package/src/codemirror/typescript/worker.ts +292 -0
- package/src/codemirror/typescript.tsx +198 -0
- package/src/create.tsx +44 -0
- package/src/icon.tsx +21 -0
- package/src/index.ts +6 -0
- package/src/logs-context.ts +5 -0
- package/src/playground.css +359 -0
- package/src/sandbox-worker/in-frame.js +179 -0
- package/src/sandbox-worker/index.ts +202 -0
- package/src/use-storage.ts +54 -0
- package/src/util.ts +29 -0
- package/src/virtual-module.d.ts +45 -0
- package/src/vite-env.d.ts +1 -0
- package/test/get-signature.test.ts +73 -0
- package/test/use-storage.test.ts +60 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import runnerWorkerURL from './typescript/runner?worker&url';
|
|
2
|
+
import * as Comlink from './comlink';
|
|
3
|
+
import type { Language } from './react';
|
|
4
|
+
import { proxy, type Remote } from './comlink';
|
|
5
|
+
import type { Logger } from '../Logs';
|
|
6
|
+
import type { RunnerAPI } from './typescript/runner';
|
|
7
|
+
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
|
8
|
+
import { StreamLanguage } from '@codemirror/language';
|
|
9
|
+
|
|
10
|
+
export async function createCurl(signal: AbortSignal, doc: string): Promise<Language> {
|
|
11
|
+
const { SandboxWorker } = await import('../sandbox-worker/index.js');
|
|
12
|
+
const { toJavaScript } = await import('./curl/curlconverter.vendor.js');
|
|
13
|
+
let runnerWorker: Worker;
|
|
14
|
+
let runnerAPI: Remote<RunnerAPI>;
|
|
15
|
+
const startRunner = () => {
|
|
16
|
+
if (runnerWorker) {
|
|
17
|
+
runnerWorker.terminate();
|
|
18
|
+
runnerAPI[Comlink.releaseProxy]();
|
|
19
|
+
}
|
|
20
|
+
runnerWorker = new SandboxWorker(runnerWorkerURL);
|
|
21
|
+
runnerAPI = Comlink.wrap<RunnerAPI>(runnerWorker);
|
|
22
|
+
runnerAPI.setLogger(
|
|
23
|
+
proxy((...args) => {
|
|
24
|
+
logger(...args);
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
startRunner();
|
|
29
|
+
|
|
30
|
+
let logger: Logger;
|
|
31
|
+
return {
|
|
32
|
+
transport: undefined,
|
|
33
|
+
fileName: 'playground.sh',
|
|
34
|
+
extensions: [StreamLanguage.define(shell)],
|
|
35
|
+
doc,
|
|
36
|
+
setLogger(fn) {
|
|
37
|
+
logger = (log) => {
|
|
38
|
+
if (log.type === 'clear') {
|
|
39
|
+
fn(log);
|
|
40
|
+
} else {
|
|
41
|
+
fn({
|
|
42
|
+
...log,
|
|
43
|
+
type: log.type === 'log' ? 'stdout' : log.type,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
async expandLogHandle(handle) {
|
|
49
|
+
return await runnerAPI.expandLogHandle(handle);
|
|
50
|
+
},
|
|
51
|
+
async freeLogHandle(handle) {
|
|
52
|
+
return await runnerAPI.freeLogHandle(handle);
|
|
53
|
+
},
|
|
54
|
+
async setEnv(vars) {
|
|
55
|
+
return await runnerAPI.setEnv(vars);
|
|
56
|
+
},
|
|
57
|
+
async run(code, signal) {
|
|
58
|
+
signal.addEventListener('abort', startRunner);
|
|
59
|
+
try {
|
|
60
|
+
const js = `
|
|
61
|
+
async function responseToCurl(res) {
|
|
62
|
+
const blob = await res.blob();
|
|
63
|
+
const bytes = new Uint8Array(await blob.arrayBuffer())
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
67
|
+
} catch {
|
|
68
|
+
text = '[output was binary: ' + URL.createObjectURL(blob) + ']'
|
|
69
|
+
}
|
|
70
|
+
const after = ['color:var(--stl-color-foreground-reduced)', '', res.status, res.statusText]
|
|
71
|
+
const headers = [\`%c< %cHTTP %s %s\`,
|
|
72
|
+
...[...res.headers.entries()].map((e) => {
|
|
73
|
+
after.push('color:var(--stl-color-foreground-reduced)')
|
|
74
|
+
after.push('')
|
|
75
|
+
after.push(e[0])
|
|
76
|
+
after.push('color:var(--stl-color-foreground-reduced)')
|
|
77
|
+
after.push('color:var(--stl-color-green-foreground)')
|
|
78
|
+
after.push(e[1])
|
|
79
|
+
after.push('')
|
|
80
|
+
return "\\n%c< %c%s%c: %c%s%c"
|
|
81
|
+
})].join("")
|
|
82
|
+
try {
|
|
83
|
+
return [headers + '\\n%c< %c\\n\\n%o', ...after, 'color:var(--stl-color-foreground-reduced)', '', JSON.parse(text)]
|
|
84
|
+
} catch {
|
|
85
|
+
return [headers + '\\n%c< %c\\n\\n%s', ...after, 'color:var(--stl-color-foreground-reduced)', '', text]
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
await (async fetch => {${toJavaScript(code)}})(async (...args) => {
|
|
89
|
+
const req = new Request(...args)
|
|
90
|
+
console.log('%c>%c %s %c%s', 'color:var(--stl-color-foreground-reduced)', '', req.method, 'color:var(--stl-color-green-foreground)', req.url)
|
|
91
|
+
console.log(...await responseToCurl(await fetch(req)))
|
|
92
|
+
})`;
|
|
93
|
+
await runnerAPI.run(js, true);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (signal.aborted) {
|
|
96
|
+
logger({ type: 'error', parts: ['Aborted.'] });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw e;
|
|
100
|
+
} finally {
|
|
101
|
+
signal.removeEventListener('abort', startRunner);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async format() {
|
|
105
|
+
throw new Error('todo');
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import './fix-lsp-markdown';
|
|
2
|
+
export * as view from '@codemirror/view';
|
|
3
|
+
export * as state from '@codemirror/state';
|
|
4
|
+
export * as language from '@codemirror/language';
|
|
5
|
+
export * as search from '@codemirror/search';
|
|
6
|
+
export * as autocomplete from '@codemirror/autocomplete';
|
|
7
|
+
export * as lint from '@codemirror/lint';
|
|
8
|
+
export * as commands from '@codemirror/commands';
|
|
9
|
+
export * as lsp from '@codemirror/lsp-client';
|
|
10
|
+
export { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
|
11
|
+
export { createLSPClient } from './lsp';
|
|
12
|
+
export { shikiHighlighter } from './shiki';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { LSPPlugin } from '@codemirror/lsp-client';
|
|
2
|
+
import { Marked } from 'marked';
|
|
3
|
+
import { sanitizeHTML } from './sanitize-html';
|
|
4
|
+
import { highlighter } from './shiki';
|
|
5
|
+
// @ts-expect-error TODO: make independent from stl-starlight
|
|
6
|
+
import { HIGHLIGHT_THEMES } from 'virtual:stl-starlight-virtual-module';
|
|
7
|
+
type MarkupKind = 'plaintext' | 'markdown';
|
|
8
|
+
interface MarkupContent {
|
|
9
|
+
/**
|
|
10
|
+
* The type of the Markup
|
|
11
|
+
*/
|
|
12
|
+
kind: MarkupKind;
|
|
13
|
+
/**
|
|
14
|
+
* The content itself
|
|
15
|
+
*/
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
18
|
+
function escHTML(text: string) {
|
|
19
|
+
return text.replace(/[\n<&]/g, (ch) => (ch == '\n' ? '<br>' : ch == '<' ? '<' : '&'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const marked = new Marked({
|
|
23
|
+
walkTokens(token) {
|
|
24
|
+
if (token.type != 'code') return;
|
|
25
|
+
token.escaped = true;
|
|
26
|
+
token.text = highlighter.codeToHtml(token.text, {
|
|
27
|
+
lang: token.lang,
|
|
28
|
+
themes: HIGHLIGHT_THEMES ?? {
|
|
29
|
+
light: 'github-light',
|
|
30
|
+
dark: 'github-dark',
|
|
31
|
+
},
|
|
32
|
+
structure: 'inline',
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export function docToHTML(value: string | MarkupContent, defaultKind: MarkupKind = 'plaintext') {
|
|
38
|
+
let kind = defaultKind,
|
|
39
|
+
text = value;
|
|
40
|
+
if (typeof text != 'string') {
|
|
41
|
+
kind = text.kind;
|
|
42
|
+
text = text.value;
|
|
43
|
+
}
|
|
44
|
+
if (kind == 'plaintext') {
|
|
45
|
+
return escHTML(text);
|
|
46
|
+
} else {
|
|
47
|
+
return sanitizeHTML(marked.parse(text, { async: false }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
LSPPlugin.prototype.docToHTML = docToHTML;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type * as lsp from 'vscode-languageserver-protocol';
|
|
2
|
+
import type { Transport } from '@codemirror/lsp-client';
|
|
3
|
+
import { sanitizeHTML } from './sanitize-html';
|
|
4
|
+
|
|
5
|
+
export interface WorkerTransportHooks {
|
|
6
|
+
onMessage?: (message: unknown) => void;
|
|
7
|
+
onSend?: (message: unknown) => unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WorkerTransportConfig {
|
|
11
|
+
worker: Worker;
|
|
12
|
+
hooks?: WorkerTransportHooks;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function workerTransport({ worker, hooks = {} }: WorkerTransportConfig): Transport {
|
|
16
|
+
let handlers: ((value: string) => void)[] = [];
|
|
17
|
+
|
|
18
|
+
worker.onmessage = (e) => {
|
|
19
|
+
if (e.data.result?.items?.[0]?.sortText) {
|
|
20
|
+
e.data.result.items.forEach((e: { detail: string; sortText: string }) => {
|
|
21
|
+
const prevDetail = e.detail;
|
|
22
|
+
e.detail = '\0' + JSON.stringify({ detail: prevDetail, sortText: e.sortText });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
hooks.onMessage?.(e.data);
|
|
26
|
+
const s = JSON.stringify(e.data);
|
|
27
|
+
for (const h of handlers) h(s);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
send(message: string) {
|
|
32
|
+
let j = JSON.parse(message);
|
|
33
|
+
if (hooks.onSend) {
|
|
34
|
+
const sent = hooks.onSend(j);
|
|
35
|
+
if (sent) {
|
|
36
|
+
j = sent;
|
|
37
|
+
} else {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
worker.postMessage(j);
|
|
42
|
+
},
|
|
43
|
+
subscribe(handler: (value: string) => void) {
|
|
44
|
+
handlers.push(handler);
|
|
45
|
+
},
|
|
46
|
+
unsubscribe(handler: (value: string) => void) {
|
|
47
|
+
handlers = handlers.filter((h) => h != handler);
|
|
48
|
+
if (handlers.length === 0) {
|
|
49
|
+
worker.terminate();
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LSPClientConfig {
|
|
56
|
+
rootUri: string;
|
|
57
|
+
onDiagnostics?: (diagnostics: lsp.Diagnostic[]) => void;
|
|
58
|
+
onNotification?: (method: string, params: unknown) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function createLSPClient(
|
|
62
|
+
LSPClient: typeof import('@codemirror/lsp-client').LSPClient,
|
|
63
|
+
config: LSPClientConfig,
|
|
64
|
+
transport: Transport,
|
|
65
|
+
) {
|
|
66
|
+
const { rootUri, onDiagnostics, onNotification } = config;
|
|
67
|
+
|
|
68
|
+
const client = new LSPClient({
|
|
69
|
+
rootUri,
|
|
70
|
+
unhandledNotification(_client: unknown, method: string, params: unknown) {
|
|
71
|
+
if (method === 'textDocument/publishDiagnostics' && onDiagnostics) {
|
|
72
|
+
const diagnosticsParams = params as lsp.PublishDiagnosticsParams;
|
|
73
|
+
onDiagnostics(diagnosticsParams.diagnostics);
|
|
74
|
+
} else if (onNotification) {
|
|
75
|
+
onNotification(method, params);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
sanitizeHTML,
|
|
79
|
+
});
|
|
80
|
+
// @ts-expect-error accessing private field
|
|
81
|
+
client.timeout = 1000 * 60;
|
|
82
|
+
client.connect(transport);
|
|
83
|
+
await client.initializing;
|
|
84
|
+
// @ts-expect-error accessing private field
|
|
85
|
+
client.timeout = 3000;
|
|
86
|
+
return client;
|
|
87
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// This file was originally written by @drudru (https://github.com/drudru/ansi_up), MIT, 2011
|
|
2
|
+
|
|
3
|
+
// Type definitions for Anser
|
|
4
|
+
// Project: https://github.com/IonicaBizau/anser
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
7
|
+
declare namespace Anser {
|
|
8
|
+
type DecorationName =
|
|
9
|
+
| 'bold'
|
|
10
|
+
| 'dim'
|
|
11
|
+
| 'italic'
|
|
12
|
+
| 'underline'
|
|
13
|
+
| 'blink'
|
|
14
|
+
| 'reverse'
|
|
15
|
+
| 'hidden'
|
|
16
|
+
| 'strikethrough';
|
|
17
|
+
|
|
18
|
+
export interface AnserJsonEntry {
|
|
19
|
+
/** The text. */
|
|
20
|
+
content: string;
|
|
21
|
+
/** The foreground color. */
|
|
22
|
+
fg: string;
|
|
23
|
+
/** The background color. */
|
|
24
|
+
bg: string;
|
|
25
|
+
/** `true` if a carriageReturn \r was fount at end of line. */
|
|
26
|
+
clearLine: boolean;
|
|
27
|
+
/** The decoration last declared before the text. */
|
|
28
|
+
decoration: null | DecorationName;
|
|
29
|
+
/** All decorations that apply to the text. */
|
|
30
|
+
decorations: Array<DecorationName>;
|
|
31
|
+
/** `true` if the colors were processed, `false` otherwise. */
|
|
32
|
+
was_processed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AnserOptions {
|
|
36
|
+
clearLine?: boolean;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ANSI_COLORS = [
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
color: 'light-dark(var(--stl-color-foreground), var(--stl-color-foreground-muted))',
|
|
44
|
+
class: 'ansi-black',
|
|
45
|
+
},
|
|
46
|
+
{ color: 'var(--stl-color-red)', class: 'ansi-red' },
|
|
47
|
+
{ color: 'var(--stl-color-green)', class: 'ansi-green' },
|
|
48
|
+
{ color: 'var(--stl-color-yellow)', class: 'ansi-yellow' },
|
|
49
|
+
{ color: 'var(--stl-color-blue)', class: 'ansi-blue' },
|
|
50
|
+
{ color: 'var(--stl-color-purple)', class: 'ansi-magenta' },
|
|
51
|
+
{ color: 'var(--stl-color-cyan)', class: 'ansi-cyan' },
|
|
52
|
+
{ color: 'var(--stl-color-foreground-reduced)', class: 'ansi-white' },
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
{ color: 'var(--stl-color-foreground-reduced)', class: 'ansi-bright-black' },
|
|
56
|
+
{ color: 'var(--stl-color-red)', class: 'ansi-bright-red' },
|
|
57
|
+
{ color: 'var(--stl-color-green)', class: 'ansi-bright-green' },
|
|
58
|
+
{
|
|
59
|
+
color: 'var(--stl-color-yellow)',
|
|
60
|
+
class: 'ansi-bright-yellow',
|
|
61
|
+
},
|
|
62
|
+
{ color: 'var(--stl-color-blue)', class: 'ansi-bright-blue' },
|
|
63
|
+
{
|
|
64
|
+
color: 'var(--stl-color-purple)',
|
|
65
|
+
class: 'ansi-bright-magenta',
|
|
66
|
+
},
|
|
67
|
+
{ color: 'var(--stl-color-cyan)', class: 'ansi-bright-cyan' },
|
|
68
|
+
{
|
|
69
|
+
color: 'light-dark(var(--stl-color-foreground-muted), var(--stl-color-foreground))',
|
|
70
|
+
class: 'ansi-bright-white',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
class Anser {
|
|
76
|
+
fg: string | null;
|
|
77
|
+
bg: string | null;
|
|
78
|
+
decorations: Anser.DecorationName[];
|
|
79
|
+
PALETTE_COLORS?: string[];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Anser
|
|
83
|
+
* The `Anser` class.
|
|
84
|
+
*
|
|
85
|
+
* @name Anser
|
|
86
|
+
* @function
|
|
87
|
+
*/
|
|
88
|
+
constructor() {
|
|
89
|
+
this.fg = this.bg = null;
|
|
90
|
+
this.decorations = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* setupPalette
|
|
95
|
+
* Sets up the palette.
|
|
96
|
+
*
|
|
97
|
+
* @name setupPalette
|
|
98
|
+
* @function
|
|
99
|
+
*/
|
|
100
|
+
setupPalette() {
|
|
101
|
+
this.PALETTE_COLORS = [];
|
|
102
|
+
|
|
103
|
+
// Index 0..15 : System color
|
|
104
|
+
for (let i = 0; i < 2; ++i) {
|
|
105
|
+
for (let j = 0; j < 8; ++j) {
|
|
106
|
+
this.PALETTE_COLORS.push(ANSI_COLORS[i]![j]!.color);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Index 16..231 : RGB 6x6x6
|
|
111
|
+
// https://gist.github.com/jasonm23/2868981#file-xterm-256color-yaml
|
|
112
|
+
const levels = [0, 95, 135, 175, 215, 255];
|
|
113
|
+
const format = (r: number, g: number, b: number) =>
|
|
114
|
+
'rgb(' + levels[r] + ', ' + levels[g] + ', ' + levels[b] + ')';
|
|
115
|
+
for (let r = 0; r < 6; ++r) {
|
|
116
|
+
for (let g = 0; g < 6; ++g) {
|
|
117
|
+
for (let b = 0; b < 6; ++b) {
|
|
118
|
+
this.PALETTE_COLORS.push(format(r, g, b));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Index 232..255 : Grayscale
|
|
124
|
+
let level = 8;
|
|
125
|
+
for (let i = 0; i < 24; ++i, level += 10) {
|
|
126
|
+
this.PALETTE_COLORS.push(format(level, level, level));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* ansiToJson
|
|
132
|
+
* Converts ANSI input into HTML output.
|
|
133
|
+
*
|
|
134
|
+
* @name ansiToJson
|
|
135
|
+
* @function
|
|
136
|
+
* @param {String} txt The input text.
|
|
137
|
+
* @param {Object} options The options passed ot the `process` method.
|
|
138
|
+
* @returns {String} The JSON output.
|
|
139
|
+
*/
|
|
140
|
+
ansiToJson(txt: string): Anser.AnserJsonEntry[] {
|
|
141
|
+
return this.process(txt, { clearLine: false }, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* process
|
|
146
|
+
* Processes the input.
|
|
147
|
+
*
|
|
148
|
+
* @name process
|
|
149
|
+
* @function
|
|
150
|
+
* @param {String} txt The input text.
|
|
151
|
+
* @param {Object} options An object passed to `processChunk` method, extended with:
|
|
152
|
+
* @param {Boolean} markup
|
|
153
|
+
*/
|
|
154
|
+
process(txt: string, options: Anser.AnserOptions, markup?: boolean): Anser.AnserJsonEntry[] {
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
156
|
+
const self = this;
|
|
157
|
+
// eslint-disable-next-line no-control-regex
|
|
158
|
+
const raw_text_chunks = txt.split(/\x1B\[/);
|
|
159
|
+
const first_chunk = raw_text_chunks.shift()!; // the first chunk is not the result of the split
|
|
160
|
+
|
|
161
|
+
if (options === undefined || options === null) {
|
|
162
|
+
options = {};
|
|
163
|
+
}
|
|
164
|
+
options.clearLine = /\r/.test(txt); // check for Carriage Return
|
|
165
|
+
const color_chunks = raw_text_chunks.map((chunk) => this.processChunk(chunk, options, markup));
|
|
166
|
+
|
|
167
|
+
const first = self.processChunkJson('');
|
|
168
|
+
first.content = first_chunk;
|
|
169
|
+
first.clearLine = options.clearLine;
|
|
170
|
+
color_chunks.unshift(first);
|
|
171
|
+
return color_chunks;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* processChunkJson
|
|
176
|
+
* Processes the current chunk into json output.
|
|
177
|
+
*
|
|
178
|
+
* @name processChunkJson
|
|
179
|
+
* @function
|
|
180
|
+
* @param {String} text The input text.
|
|
181
|
+
* @param {Object} options An object containing the following fields:
|
|
182
|
+
* @param {Boolean} markup If false, the colors will not be parsed.
|
|
183
|
+
* @return {Object} The result object:
|
|
184
|
+
*
|
|
185
|
+
* - `content` (String): The text.
|
|
186
|
+
* - `fg` (String|null): The foreground color.
|
|
187
|
+
* - `bg` (String|null): The background color.
|
|
188
|
+
* - `clearLine` (Boolean): `true` if a carriageReturn \r was fount at end of line.
|
|
189
|
+
* - `was_processed` (Boolean): `true` if the colors were processed, `false` otherwise.
|
|
190
|
+
*/
|
|
191
|
+
processChunkJson(text: string, options?: Anser.AnserOptions, markup?: boolean): Anser.AnserJsonEntry {
|
|
192
|
+
// Are we using classes or styles?
|
|
193
|
+
options = typeof options == 'undefined' ? {} : options;
|
|
194
|
+
|
|
195
|
+
const result: Anser.AnserJsonEntry = {
|
|
196
|
+
content: text,
|
|
197
|
+
fg: '',
|
|
198
|
+
bg: '',
|
|
199
|
+
clearLine: options.clearLine ?? false,
|
|
200
|
+
decoration: null,
|
|
201
|
+
decorations: [],
|
|
202
|
+
was_processed: false,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Each "chunk" is the text after the CSI (ESC + "[") and before the next CSI/EOF.
|
|
206
|
+
//
|
|
207
|
+
// This regex matches four groups within a chunk.
|
|
208
|
+
//
|
|
209
|
+
// The first and third groups match code type.
|
|
210
|
+
// We supported only SGR command. It has empty first group and "m" in third.
|
|
211
|
+
//
|
|
212
|
+
// The second group matches all of the number+semicolon command sequences
|
|
213
|
+
// before the "m" (or other trailing) character.
|
|
214
|
+
// These are the graphics or SGR commands.
|
|
215
|
+
//
|
|
216
|
+
// The last group is the text (including newlines) that is colored by
|
|
217
|
+
// the other group"s commands.
|
|
218
|
+
const matches = text.match(/^([!\x3c-\x3f]*)([\d;]*)([\x20-\x2c]*[\x40-\x7e])([\s\S]*)/m);
|
|
219
|
+
|
|
220
|
+
if (!matches) return result;
|
|
221
|
+
|
|
222
|
+
result.content = matches[4]!;
|
|
223
|
+
const numbers = matches[2]!.split(';');
|
|
224
|
+
|
|
225
|
+
// We currently support only "SGR" (Select Graphic Rendition)
|
|
226
|
+
// Simply ignore if not a SGR command.
|
|
227
|
+
if (matches[1] !== '' || matches[3] !== 'm') {
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!markup) {
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
236
|
+
const self = this;
|
|
237
|
+
|
|
238
|
+
while (numbers.length > 0) {
|
|
239
|
+
const num_str = numbers.shift()!;
|
|
240
|
+
const num = parseInt(num_str);
|
|
241
|
+
|
|
242
|
+
if (isNaN(num) || num === 0) {
|
|
243
|
+
self.fg = self.bg = null;
|
|
244
|
+
self.decorations = [];
|
|
245
|
+
} else if (num === 1) {
|
|
246
|
+
self.decorations.push('bold');
|
|
247
|
+
} else if (num === 2) {
|
|
248
|
+
self.decorations.push('dim');
|
|
249
|
+
// Enable code 2 to get string
|
|
250
|
+
} else if (num === 3) {
|
|
251
|
+
self.decorations.push('italic');
|
|
252
|
+
} else if (num === 4) {
|
|
253
|
+
self.decorations.push('underline');
|
|
254
|
+
} else if (num === 5) {
|
|
255
|
+
self.decorations.push('blink');
|
|
256
|
+
} else if (num === 7) {
|
|
257
|
+
self.decorations.push('reverse');
|
|
258
|
+
} else if (num === 8) {
|
|
259
|
+
self.decorations.push('hidden');
|
|
260
|
+
// Enable code 9 to get strikethrough
|
|
261
|
+
} else if (num === 9) {
|
|
262
|
+
self.decorations.push('strikethrough');
|
|
263
|
+
/**
|
|
264
|
+
* Add several widely used style codes
|
|
265
|
+
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
|
|
266
|
+
*/
|
|
267
|
+
} else if (num === 21) {
|
|
268
|
+
self.removeDecoration('bold');
|
|
269
|
+
} else if (num === 22) {
|
|
270
|
+
self.removeDecoration('bold');
|
|
271
|
+
self.removeDecoration('dim');
|
|
272
|
+
} else if (num === 23) {
|
|
273
|
+
self.removeDecoration('italic');
|
|
274
|
+
} else if (num === 24) {
|
|
275
|
+
self.removeDecoration('underline');
|
|
276
|
+
} else if (num === 25) {
|
|
277
|
+
self.removeDecoration('blink');
|
|
278
|
+
} else if (num === 27) {
|
|
279
|
+
self.removeDecoration('reverse');
|
|
280
|
+
} else if (num === 28) {
|
|
281
|
+
self.removeDecoration('hidden');
|
|
282
|
+
} else if (num === 29) {
|
|
283
|
+
self.removeDecoration('strikethrough');
|
|
284
|
+
} else if (num === 39) {
|
|
285
|
+
self.fg = null;
|
|
286
|
+
} else if (num === 49) {
|
|
287
|
+
self.bg = null;
|
|
288
|
+
// Foreground color
|
|
289
|
+
} else if (num >= 30 && num < 38) {
|
|
290
|
+
self.fg = ANSI_COLORS[0]![num % 10]!.color;
|
|
291
|
+
// Foreground bright color
|
|
292
|
+
} else if (num >= 90 && num < 98) {
|
|
293
|
+
self.fg = ANSI_COLORS[1]![num % 10]!.color;
|
|
294
|
+
// Background color
|
|
295
|
+
} else if (num >= 40 && num < 48) {
|
|
296
|
+
self.bg = ANSI_COLORS[0]![num % 10]!.color;
|
|
297
|
+
// Background bright color
|
|
298
|
+
} else if (num >= 100 && num < 108) {
|
|
299
|
+
self.bg = ANSI_COLORS[1]![num % 10]!.color;
|
|
300
|
+
} else if (num === 38 || num === 48) {
|
|
301
|
+
// extend color (38=fg, 48=bg)
|
|
302
|
+
const is_foreground = num === 38;
|
|
303
|
+
if (numbers.length >= 1) {
|
|
304
|
+
const mode = numbers.shift();
|
|
305
|
+
if (mode === '5' && numbers.length >= 1) {
|
|
306
|
+
// palette color
|
|
307
|
+
const palette_index = parseInt(numbers.shift()!);
|
|
308
|
+
if (palette_index >= 0 && palette_index <= 255) {
|
|
309
|
+
if (!this.PALETTE_COLORS) {
|
|
310
|
+
self.setupPalette();
|
|
311
|
+
}
|
|
312
|
+
if (is_foreground) {
|
|
313
|
+
self.fg = this.PALETTE_COLORS![palette_index]!;
|
|
314
|
+
} else {
|
|
315
|
+
self.bg = this.PALETTE_COLORS![palette_index]!;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} else if (mode === '2' && numbers.length >= 3) {
|
|
319
|
+
// true color
|
|
320
|
+
const r = parseInt(numbers.shift()!);
|
|
321
|
+
const g = parseInt(numbers.shift()!);
|
|
322
|
+
const b = parseInt(numbers.shift()!);
|
|
323
|
+
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
324
|
+
const color = 'rgb(' + r + ', ' + g + ', ' + b + ')';
|
|
325
|
+
if (is_foreground) {
|
|
326
|
+
self.fg = color;
|
|
327
|
+
} else {
|
|
328
|
+
self.bg = color;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (self.fg === null && self.bg === null && self.decorations.length === 0) {
|
|
337
|
+
return result;
|
|
338
|
+
} else {
|
|
339
|
+
result.fg = self.fg!;
|
|
340
|
+
result.bg = self.bg!;
|
|
341
|
+
result.decorations = self.decorations;
|
|
342
|
+
result.decoration = self.decorations.slice(-1).pop() || null;
|
|
343
|
+
result.was_processed = true;
|
|
344
|
+
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* processChunk
|
|
351
|
+
* Processes the current chunk of text.
|
|
352
|
+
*
|
|
353
|
+
* @name processChunk
|
|
354
|
+
* @function
|
|
355
|
+
* @param {String} text The input text.
|
|
356
|
+
* @param {Object} options An object containing the following fields:
|
|
357
|
+
*
|
|
358
|
+
* - `json` (Boolean): If `true`, the result will be an object.
|
|
359
|
+
* - `use_classes` (Boolean): If `true`, HTML classes will be appended to the HTML output.
|
|
360
|
+
*
|
|
361
|
+
* @param {Boolean} markup If false, the colors will not be parsed.
|
|
362
|
+
* @return {Object|String} The result (object if `json` is wanted back or string otherwise).
|
|
363
|
+
*/
|
|
364
|
+
processChunk(text: string, options: Anser.AnserOptions, markup?: boolean): Anser.AnserJsonEntry {
|
|
365
|
+
options = options || {};
|
|
366
|
+
const jsonChunk = this.processChunkJson(text, options, markup ?? false);
|
|
367
|
+
|
|
368
|
+
// "reverse" decoration reverses foreground and background colors
|
|
369
|
+
jsonChunk.decorations = jsonChunk.decorations.filter((decoration) => {
|
|
370
|
+
if (decoration === 'reverse') {
|
|
371
|
+
// when reversing, missing colors are defaulted to black (bg) and white (fg)
|
|
372
|
+
if (!jsonChunk.fg) {
|
|
373
|
+
jsonChunk.fg = ANSI_COLORS[0]![7]!.color;
|
|
374
|
+
}
|
|
375
|
+
if (!jsonChunk.bg) {
|
|
376
|
+
jsonChunk.bg = ANSI_COLORS[0]![0]!.color;
|
|
377
|
+
}
|
|
378
|
+
const tmpFg = jsonChunk.fg;
|
|
379
|
+
jsonChunk.fg = jsonChunk.bg;
|
|
380
|
+
jsonChunk.bg = tmpFg;
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return jsonChunk;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
removeDecoration(decoration: Anser.DecorationName) {
|
|
390
|
+
const index = this.decorations.indexOf(decoration);
|
|
391
|
+
|
|
392
|
+
if (index >= 0) {
|
|
393
|
+
this.decorations.splice(index, 1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default Anser;
|