@jsenv/terminal-recorder 1.5.8 → 1.5.9
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/dist/client/jsenv_terminal_recorder_node_modules.js +753 -803
- package/dist/{xterm.html → client/xterm.html} +2 -2
- package/dist/jsenv_terminal_recorder_node_modules.js +773 -753
- package/dist/{client/terminal_recorder_browser.js → terminal_recorder_browser.js} +121 -23
- package/package.json +3 -4
- package/src/svg/parse_ansi.js +9 -8
- package/src/svg/render_terminal_svg.js +17 -15
- /package/dist/{css → client/css}/xterm.css +0 -0
|
@@ -1,4 +1,99 @@
|
|
|
1
|
-
import { stripAnsi, ansiRegex,
|
|
1
|
+
import { emojiRegex, eastAsianWidth, stripAnsi, ansiRegex, __jsenv_default_import__ } from "./jsenv_terminal_recorder_node_modules.js";
|
|
2
|
+
|
|
3
|
+
const createMeasureTextWidth = ({ stripAnsi }) => {
|
|
4
|
+
const segmenter = new Intl.Segmenter();
|
|
5
|
+
const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
|
|
6
|
+
|
|
7
|
+
const measureTextWidth = (
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
ambiguousIsNarrow = true,
|
|
11
|
+
countAnsiEscapeCodes = false,
|
|
12
|
+
skipEmojis = false,
|
|
13
|
+
} = {},
|
|
14
|
+
) => {
|
|
15
|
+
if (typeof string !== "string" || string.length === 0) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!countAnsiEscapeCodes) {
|
|
20
|
+
string = stripAnsi(string);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (string.length === 0) {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let width = 0;
|
|
28
|
+
const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
|
|
29
|
+
|
|
30
|
+
for (const { segment: character } of segmenter.segment(string)) {
|
|
31
|
+
const codePoint = character.codePointAt(0);
|
|
32
|
+
|
|
33
|
+
// Ignore control characters
|
|
34
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ignore zero-width characters
|
|
39
|
+
if (
|
|
40
|
+
(codePoint >= 0x20_0b && codePoint <= 0x20_0f) || // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
|
|
41
|
+
codePoint === 0xfe_ff // Zero-width no-break space
|
|
42
|
+
) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ignore combining characters
|
|
47
|
+
if (
|
|
48
|
+
(codePoint >= 0x3_00 && codePoint <= 0x3_6f) || // Combining diacritical marks
|
|
49
|
+
(codePoint >= 0x1a_b0 && codePoint <= 0x1a_ff) || // Combining diacritical marks extended
|
|
50
|
+
(codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || // Combining diacritical marks supplement
|
|
51
|
+
(codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || // Combining diacritical marks for symbols
|
|
52
|
+
(codePoint >= 0xfe_20 && codePoint <= 0xfe_2f) // Combining half marks
|
|
53
|
+
) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ignore surrogate pairs
|
|
58
|
+
if (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Ignore variation selectors
|
|
63
|
+
if (codePoint >= 0xfe_00 && codePoint <= 0xfe_0f) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// This covers some of the above cases, but we still keep them for performance reasons.
|
|
68
|
+
if (defaultIgnorableCodePointRegex.test(character)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!skipEmojis && emojiRegex().test(character)) {
|
|
73
|
+
if (process.env.CAPTURING_SIDE_EFFECTS) {
|
|
74
|
+
if (character === "✔️") {
|
|
75
|
+
width += 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
width += measureTextWidth(character, {
|
|
80
|
+
skipEmojis: true,
|
|
81
|
+
countAnsiEscapeCodes: true, // to skip call to stripAnsi
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return width;
|
|
90
|
+
};
|
|
91
|
+
return measureTextWidth;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const measureTextWidth = createMeasureTextWidth({
|
|
95
|
+
stripAnsi,
|
|
96
|
+
});
|
|
2
97
|
|
|
3
98
|
// inspired by https://github.com/F1LT3R/parse-ansi/blob/master/index.js
|
|
4
99
|
|
|
@@ -10,12 +105,12 @@ const parseAnsi = (ansi) => {
|
|
|
10
105
|
const lines = plainText.split("\n");
|
|
11
106
|
const rows = lines.length;
|
|
12
107
|
let columns = 0;
|
|
13
|
-
|
|
108
|
+
for (const line of lines) {
|
|
14
109
|
const len = line.length;
|
|
15
110
|
if (len > columns) {
|
|
16
111
|
columns = len;
|
|
17
112
|
}
|
|
18
|
-
}
|
|
113
|
+
}
|
|
19
114
|
|
|
20
115
|
const result = {
|
|
21
116
|
raw: ansi,
|
|
@@ -156,7 +251,7 @@ const parseAnsi = (ansi) => {
|
|
|
156
251
|
let nAnsi = 0;
|
|
157
252
|
let nPlain = 0;
|
|
158
253
|
|
|
159
|
-
const bundle = (type, value) => {
|
|
254
|
+
const bundle = (type, value, { width = 0, height = 0 } = {}) => {
|
|
160
255
|
const chunk = {
|
|
161
256
|
type,
|
|
162
257
|
value,
|
|
@@ -165,9 +260,10 @@ const parseAnsi = (ansi) => {
|
|
|
165
260
|
y,
|
|
166
261
|
n: nPlain,
|
|
167
262
|
raw: nAnsi,
|
|
263
|
+
width,
|
|
264
|
+
height,
|
|
168
265
|
},
|
|
169
266
|
};
|
|
170
|
-
|
|
171
267
|
if (type === "text" || type === "ansi") {
|
|
172
268
|
const style = {};
|
|
173
269
|
|
|
@@ -217,7 +313,7 @@ const parseAnsi = (ansi) => {
|
|
|
217
313
|
for (const word of words) {
|
|
218
314
|
// Newline character
|
|
219
315
|
if (word === "\n") {
|
|
220
|
-
const chunk = bundle("newline", "\n");
|
|
316
|
+
const chunk = bundle("newline", "\n", { height: 1 });
|
|
221
317
|
result.chunks.push(chunk);
|
|
222
318
|
x = 0;
|
|
223
319
|
y += 1;
|
|
@@ -227,9 +323,9 @@ const parseAnsi = (ansi) => {
|
|
|
227
323
|
}
|
|
228
324
|
// Text characters
|
|
229
325
|
if (delimiters.includes(word) === false) {
|
|
230
|
-
const
|
|
326
|
+
const width = measureTextWidth(word);
|
|
327
|
+
const chunk = bundle("text", word, { width });
|
|
231
328
|
result.chunks.push(chunk);
|
|
232
|
-
const width = stringWidth(word);
|
|
233
329
|
x += width;
|
|
234
330
|
nAnsi += width;
|
|
235
331
|
nPlain += width;
|
|
@@ -747,11 +843,24 @@ const renderTerminalSvg = (
|
|
|
747
843
|
continue;
|
|
748
844
|
}
|
|
749
845
|
const { position } = chunk;
|
|
750
|
-
const x =
|
|
751
|
-
offsetLeft + (position.x + leadingWhitespaceWidth(value)) * font.width;
|
|
752
|
-
const y = offsetTop + position.y + font.lineHeight * position.y;
|
|
753
|
-
const w = font.width * value.length;
|
|
754
846
|
const attrs = {};
|
|
847
|
+
|
|
848
|
+
// Some SVG Implementations drop whitespaces
|
|
849
|
+
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space
|
|
850
|
+
// and https://codepen.io/dmail/pen/wvLvbbP
|
|
851
|
+
if (
|
|
852
|
+
// in the following case we need to preserve whitespaces:
|
|
853
|
+
// - one or more leading whitespace
|
|
854
|
+
// - one or more trailing whitespace
|
|
855
|
+
// - 2 or more whitespace in the middle of the string
|
|
856
|
+
/^\s+|\s\s|\s+$/.test(value)
|
|
857
|
+
) {
|
|
858
|
+
attrs.style = "white-space:pre";
|
|
859
|
+
}
|
|
860
|
+
const x = offsetLeft + position.x * font.width;
|
|
861
|
+
const y = offsetTop + position.y + font.lineHeight * position.y;
|
|
862
|
+
const w = font.width * position.width;
|
|
863
|
+
|
|
755
864
|
if (style.bold) {
|
|
756
865
|
attrs["font-weight"] = "bold";
|
|
757
866
|
}
|
|
@@ -823,15 +932,4 @@ const renderTerminalSvg = (
|
|
|
823
932
|
return svgString;
|
|
824
933
|
};
|
|
825
934
|
|
|
826
|
-
// Some SVG Implementations drop whitespaces
|
|
827
|
-
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space
|
|
828
|
-
// and https://codepen.io/dmail/pen/wvLvbbP
|
|
829
|
-
const leadingWhitespaceWidth = (text) => {
|
|
830
|
-
if (text.trim() === "") {
|
|
831
|
-
return 0;
|
|
832
|
-
}
|
|
833
|
-
const leadingSpace = text.match(/^\s*/g);
|
|
834
|
-
return leadingSpace[0].length;
|
|
835
|
-
};
|
|
836
|
-
|
|
837
935
|
export { parseAnsi, renderTerminalSvg };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/terminal-recorder",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
4
|
"description": "Record terminal output as .svg, .gif, .webm, .mp4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,13 +37,12 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@jsenv/humanize": "1.
|
|
41
|
-
"@jsenv/server": "16.0.
|
|
40
|
+
"@jsenv/humanize": "1.4.0",
|
|
41
|
+
"@jsenv/server": "16.0.6",
|
|
42
42
|
"@playwright/browser-chromium": "1.51.0",
|
|
43
43
|
"ansi-regex": "6.1.0",
|
|
44
44
|
"he": "1.2.0",
|
|
45
45
|
"playwright": "1.51.0",
|
|
46
|
-
"string-width": "5.1.2",
|
|
47
46
|
"strip-ansi": "7.1.0",
|
|
48
47
|
"eastasianwidth": "0.3.0",
|
|
49
48
|
"webm-to-mp4": "1.0.0"
|
package/src/svg/parse_ansi.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// inspired by https://github.com/F1LT3R/parse-ansi/blob/master/index.js
|
|
2
2
|
|
|
3
|
+
import { measureTextWidth } from "@jsenv/terminal-text-size";
|
|
3
4
|
import ansiRegex from "ansi-regex";
|
|
4
|
-
import stringWidth from "string-width";
|
|
5
5
|
import stripAnsi from "strip-ansi";
|
|
6
6
|
|
|
7
7
|
export const parseAnsi = (ansi) => {
|
|
@@ -11,12 +11,12 @@ export const parseAnsi = (ansi) => {
|
|
|
11
11
|
const lines = plainText.split("\n");
|
|
12
12
|
const rows = lines.length;
|
|
13
13
|
let columns = 0;
|
|
14
|
-
|
|
14
|
+
for (const line of lines) {
|
|
15
15
|
const len = line.length;
|
|
16
16
|
if (len > columns) {
|
|
17
17
|
columns = len;
|
|
18
18
|
}
|
|
19
|
-
}
|
|
19
|
+
}
|
|
20
20
|
|
|
21
21
|
const result = {
|
|
22
22
|
raw: ansi,
|
|
@@ -158,7 +158,7 @@ export const parseAnsi = (ansi) => {
|
|
|
158
158
|
let nAnsi = 0;
|
|
159
159
|
let nPlain = 0;
|
|
160
160
|
|
|
161
|
-
const bundle = (type, value) => {
|
|
161
|
+
const bundle = (type, value, { width = 0, height = 0 } = {}) => {
|
|
162
162
|
const chunk = {
|
|
163
163
|
type,
|
|
164
164
|
value,
|
|
@@ -167,9 +167,10 @@ export const parseAnsi = (ansi) => {
|
|
|
167
167
|
y,
|
|
168
168
|
n: nPlain,
|
|
169
169
|
raw: nAnsi,
|
|
170
|
+
width,
|
|
171
|
+
height,
|
|
170
172
|
},
|
|
171
173
|
};
|
|
172
|
-
|
|
173
174
|
if (type === "text" || type === "ansi") {
|
|
174
175
|
const style = {};
|
|
175
176
|
|
|
@@ -219,7 +220,7 @@ export const parseAnsi = (ansi) => {
|
|
|
219
220
|
for (const word of words) {
|
|
220
221
|
// Newline character
|
|
221
222
|
if (word === "\n") {
|
|
222
|
-
const chunk = bundle("newline", "\n");
|
|
223
|
+
const chunk = bundle("newline", "\n", { height: 1 });
|
|
223
224
|
result.chunks.push(chunk);
|
|
224
225
|
x = 0;
|
|
225
226
|
y += 1;
|
|
@@ -229,9 +230,9 @@ export const parseAnsi = (ansi) => {
|
|
|
229
230
|
}
|
|
230
231
|
// Text characters
|
|
231
232
|
if (delimiters.includes(word) === false) {
|
|
232
|
-
const
|
|
233
|
+
const width = measureTextWidth(word);
|
|
234
|
+
const chunk = bundle("text", word, { width });
|
|
233
235
|
result.chunks.push(chunk);
|
|
234
|
-
const width = stringWidth(word);
|
|
235
236
|
x += width;
|
|
236
237
|
nAnsi += width;
|
|
237
238
|
nPlain += width;
|
|
@@ -221,11 +221,24 @@ export const renderTerminalSvg = (
|
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
223
|
const { position } = chunk;
|
|
224
|
-
const x =
|
|
225
|
-
offsetLeft + (position.x + leadingWhitespaceWidth(value)) * font.width;
|
|
226
|
-
const y = offsetTop + position.y + font.lineHeight * position.y;
|
|
227
|
-
const w = font.width * value.length;
|
|
228
224
|
const attrs = {};
|
|
225
|
+
|
|
226
|
+
// Some SVG Implementations drop whitespaces
|
|
227
|
+
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space
|
|
228
|
+
// and https://codepen.io/dmail/pen/wvLvbbP
|
|
229
|
+
if (
|
|
230
|
+
// in the following case we need to preserve whitespaces:
|
|
231
|
+
// - one or more leading whitespace
|
|
232
|
+
// - one or more trailing whitespace
|
|
233
|
+
// - 2 or more whitespace in the middle of the string
|
|
234
|
+
/^\s+|\s\s|\s+$/.test(value)
|
|
235
|
+
) {
|
|
236
|
+
attrs.style = "white-space:pre";
|
|
237
|
+
}
|
|
238
|
+
const x = offsetLeft + position.x * font.width;
|
|
239
|
+
const y = offsetTop + position.y + font.lineHeight * position.y;
|
|
240
|
+
const w = font.width * position.width;
|
|
241
|
+
|
|
229
242
|
if (style.bold) {
|
|
230
243
|
attrs["font-weight"] = "bold";
|
|
231
244
|
}
|
|
@@ -297,14 +310,3 @@ export const renderTerminalSvg = (
|
|
|
297
310
|
const svgString = svg.renderAsString();
|
|
298
311
|
return svgString;
|
|
299
312
|
};
|
|
300
|
-
|
|
301
|
-
// Some SVG Implementations drop whitespaces
|
|
302
|
-
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space
|
|
303
|
-
// and https://codepen.io/dmail/pen/wvLvbbP
|
|
304
|
-
const leadingWhitespaceWidth = (text) => {
|
|
305
|
-
if (text.trim() === "") {
|
|
306
|
-
return 0;
|
|
307
|
-
}
|
|
308
|
-
const leadingSpace = text.match(/^\s*/g);
|
|
309
|
-
return leadingSpace[0].length;
|
|
310
|
-
};
|
|
File without changes
|