CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/components/ansi-to-react.ts
Views: 687
1
/*
2
This is a fork of the BSD-licensed https://github.com/nteract/ansi-to-react
3
4
It should stay BSD licensed, NOT switched to cocalc's less open license.
5
*/
6
7
import Anser, { AnserJsonEntry } from "anser";
8
import { escapeCarriageReturn } from "escape-carriage";
9
import * as React from "react";
10
11
/**
12
* Converts ANSI strings into JSON output.
13
* @name ansiToJSON
14
* @function
15
* @param {String} input The input string.
16
* @param {boolean} use_classes If `true`, HTML classes will be appended
17
* to the HTML output.
18
* @return {Array} The parsed input.
19
*/
20
function ansiToJSON(
21
input: string,
22
use_classes: boolean = false
23
): AnserJsonEntry[] {
24
input = escapeCarriageReturn(fixBackspace(input));
25
return Anser.ansiToJson(input, {
26
json: true,
27
remove_empty: true,
28
use_classes,
29
});
30
}
31
32
/**
33
* Create a class string.
34
* @name createClass
35
* @function
36
* @param {AnserJsonEntry} bundle
37
* @return {String} class name(s)
38
*/
39
function createClass(bundle: AnserJsonEntry): string | null {
40
let classNames: string = "";
41
42
if (bundle.bg) {
43
classNames += `${bundle.bg}-bg `;
44
}
45
if (bundle.fg) {
46
classNames += `${bundle.fg}-fg `;
47
}
48
if (bundle.decoration) {
49
classNames += `ansi-${bundle.decoration} `;
50
}
51
52
if (classNames === "") {
53
return null;
54
}
55
56
classNames = classNames.substring(0, classNames.length - 1);
57
return classNames;
58
}
59
60
/**
61
* Create the style attribute.
62
* @name createStyle
63
* @function
64
* @param {AnserJsonEntry} bundle
65
* @return {Object} returns the style object
66
*/
67
function createStyle(bundle: AnserJsonEntry): React.CSSProperties {
68
const style: React.CSSProperties = {};
69
if (bundle.bg) {
70
style.backgroundColor = `rgb(${bundle.bg})`;
71
}
72
if (bundle.fg) {
73
style.color = `rgb(${bundle.fg})`;
74
}
75
switch (bundle.decoration) {
76
case 'bold':
77
style.fontWeight = 'bold';
78
break;
79
case 'dim':
80
style.opacity = '0.5';
81
break;
82
case 'italic':
83
style.fontStyle = 'italic';
84
break;
85
case 'hidden':
86
style.visibility = 'hidden';
87
break;
88
case 'strikethrough':
89
style.textDecoration = 'line-through';
90
break;
91
case 'underline':
92
style.textDecoration = 'underline';
93
break;
94
case 'blink':
95
style.textDecoration = 'blink';
96
break;
97
default:
98
break;
99
}
100
return style;
101
}
102
103
/**
104
* Converts an Anser bundle into a React Node.
105
* @param linkify whether links should be converting into clickable anchor tags.
106
* @param useClasses should render the span with a class instead of style.
107
* @param bundle Anser output.
108
* @param key
109
*/
110
111
function convertBundleIntoReact(
112
linkify: boolean,
113
useClasses: boolean,
114
bundle: AnserJsonEntry,
115
key: number
116
): JSX.Element {
117
const style = useClasses ? null : createStyle(bundle);
118
const className = useClasses ? createClass(bundle) : null;
119
120
if (!linkify) {
121
return React.createElement(
122
"span",
123
{ style, key, className },
124
bundle.content
125
);
126
}
127
128
const content: React.ReactNode[] = [];
129
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
130
131
let index = 0;
132
let match: RegExpExecArray | null;
133
while ((match = linkRegex.exec(bundle.content)) !== null) {
134
const [, pre, url] = match;
135
136
const startIndex = match.index + pre.length;
137
if (startIndex > index) {
138
content.push(bundle.content.substring(index, startIndex));
139
}
140
141
// Make sure the href we generate from the link is fully qualified. We assume http
142
// if it starts with a www because many sites don't support https
143
const href = url.startsWith("www.") ? `http://${url}` : url;
144
content.push(
145
React.createElement(
146
"a",
147
{
148
key: index,
149
href,
150
target: "_blank",
151
},
152
`${url}`
153
)
154
);
155
156
index = linkRegex.lastIndex;
157
}
158
159
if (index < bundle.content.length) {
160
content.push(bundle.content.substring(index));
161
}
162
163
return React.createElement("span", { style, key, className }, content);
164
}
165
166
declare interface Props {
167
children?: string;
168
linkify?: boolean;
169
className?: string;
170
useClasses?: boolean;
171
}
172
173
export default function Ansi(props: Props): JSX.Element {
174
const { className, useClasses, children, linkify } = props;
175
return React.createElement(
176
"code",
177
{ className },
178
ansiToJSON(children ?? "", useClasses ?? false).map(
179
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
180
)
181
);
182
}
183
184
// This is copied from the Jupyter Classic source code
185
// notebook/static/base/js/utils.js to handle \b in a way
186
// that is **compatible with Jupyter classic**. One can
187
// argue that this behavior is questionable:
188
// https://stackoverflow.com/questions/55440152/multiple-b-doesnt-work-as-expected-in-jupyter#
189
function fixBackspace(txt: string) {
190
let tmp = txt;
191
do {
192
txt = tmp;
193
// Cancel out anything-but-newline followed by backspace
194
tmp = txt.replace(/[^\n]\x08/gm, "");
195
} while (tmp.length < txt.length);
196
return txt;
197
}
198
199
200