@jsenv/terminal-recorder 1.0.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/README.md +51 -0
- package/dist/css/xterm.css +177 -0
- package/dist/js/terminal_recorder.js +1470 -0
- package/dist/xterm.html +26 -0
- package/package.json +47 -0
- package/src/client/gif_encoder.js +398 -0
- package/src/client/gif_encoder_dev/gif_encoder.js +567 -0
- package/src/client/gif_encoder_dev/gif_encoder_dev.html +130 -0
- package/src/client/gif_encoder_dev/lzwencoder.js +275 -0
- package/src/client/gif_encoder_dev/neuquant.js +510 -0
- package/src/client/gif_encoder_jsenv_dev/gif_encoder_jsenv_dev.html +94 -0
- package/src/client/lzw_encoder.js +253 -0
- package/src/client/neuquant.js +498 -0
- package/src/client/terminal_recorder.js +317 -0
- package/src/client/xterm.html +26 -0
- package/src/client/xterm_dev.html +48 -0
- package/src/main.js +4 -0
- package/src/svg/parse_ansi.js +375 -0
- package/src/svg/render_terminal_html.js +5 -0
- package/src/svg/render_terminal_svg.js +300 -0
- package/src/svg/xml_generator.js +149 -0
- package/src/terminal_recording.js +205 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// inspired by https://github.com/F1LT3R/parse-ansi/blob/master/index.js
|
|
2
|
+
|
|
3
|
+
import ansiRegex from "ansi-regex";
|
|
4
|
+
import stripAnsi from "strip-ansi";
|
|
5
|
+
|
|
6
|
+
export const parseAnsi = (ansi) => {
|
|
7
|
+
ansi = ansi.replace(/\r\n/g, "\n"); // normalize windows line endings
|
|
8
|
+
|
|
9
|
+
const plainText = stripAnsi(ansi);
|
|
10
|
+
const lines = plainText.split("\n");
|
|
11
|
+
const rows = lines.length;
|
|
12
|
+
let columns = 0;
|
|
13
|
+
lines.forEach((line) => {
|
|
14
|
+
const len = line.length;
|
|
15
|
+
if (len > columns) {
|
|
16
|
+
columns = len;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const result = {
|
|
21
|
+
raw: ansi,
|
|
22
|
+
plainText,
|
|
23
|
+
rows,
|
|
24
|
+
columns,
|
|
25
|
+
chunks: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let words;
|
|
29
|
+
const delimiters = [];
|
|
30
|
+
{
|
|
31
|
+
const matches = ansi.match(ansiRegex()) || [];
|
|
32
|
+
for (const match of matches) {
|
|
33
|
+
if (!delimiters.includes(match)) {
|
|
34
|
+
delimiters.push(match);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
delimiters.push("\n");
|
|
38
|
+
|
|
39
|
+
const splitString = (str, delimiter) => {
|
|
40
|
+
const result = [];
|
|
41
|
+
let index = 0;
|
|
42
|
+
const parts = str.split(delimiter);
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
result.push(part);
|
|
45
|
+
if (index < parts.length - 1) {
|
|
46
|
+
result.push(delimiter);
|
|
47
|
+
}
|
|
48
|
+
index++;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
const splitArray = (array, delimiter) => {
|
|
53
|
+
let result = [];
|
|
54
|
+
for (const part of array) {
|
|
55
|
+
let subRes = splitString(part, delimiter);
|
|
56
|
+
subRes = subRes.filter((str) => {
|
|
57
|
+
return Boolean(str);
|
|
58
|
+
});
|
|
59
|
+
result = result.concat(subRes);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
const superSplit = (splittable, delimiters) => {
|
|
64
|
+
if (delimiters.length === 0) {
|
|
65
|
+
return splittable;
|
|
66
|
+
}
|
|
67
|
+
if (typeof splittable === "string") {
|
|
68
|
+
const delimiter = delimiters[delimiters.length - 1];
|
|
69
|
+
const split = splitString(splittable, delimiter);
|
|
70
|
+
return superSplit(split, delimiters.slice(0, -1));
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(splittable)) {
|
|
73
|
+
const delimiter = delimiters[delimiters.length - 1];
|
|
74
|
+
const split = splitArray(splittable, delimiter);
|
|
75
|
+
return superSplit(split, delimiters.slice(0, -1));
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
words = superSplit(ansi, delimiters);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const styleStack = {
|
|
83
|
+
foregroundColor: [],
|
|
84
|
+
backgroundColor: [],
|
|
85
|
+
boldDim: [],
|
|
86
|
+
};
|
|
87
|
+
const getForegroundColor = () => {
|
|
88
|
+
if (styleStack.foregroundColor.length > 0) {
|
|
89
|
+
return styleStack.foregroundColor[styleStack.foregroundColor.length - 1];
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
const getBackgroundColor = () => {
|
|
94
|
+
if (styleStack.backgroundColor.length > 0) {
|
|
95
|
+
return styleStack.backgroundColor[styleStack.backgroundColor.length - 1];
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
const getDim = () => {
|
|
100
|
+
return styleStack.boldDim.includes("dim");
|
|
101
|
+
};
|
|
102
|
+
const getBold = () => {
|
|
103
|
+
return styleStack.boldDim.includes("bold");
|
|
104
|
+
};
|
|
105
|
+
const styleState = {
|
|
106
|
+
italic: false,
|
|
107
|
+
underline: false,
|
|
108
|
+
inverse: false,
|
|
109
|
+
hidden: false,
|
|
110
|
+
strikethrough: false,
|
|
111
|
+
};
|
|
112
|
+
const decoratorEffects = {
|
|
113
|
+
foregroundColorOpen: (color) => styleStack.foregroundColor.push(color),
|
|
114
|
+
foregroundColorClose: () => styleStack.foregroundColor.pop(),
|
|
115
|
+
backgroundColorOpen: (color) => styleStack.backgroundColor.push(color),
|
|
116
|
+
backgroundColorClose: () => styleStack.backgroundColor.pop(),
|
|
117
|
+
boldOpen: () => styleStack.boldDim.push("bold"),
|
|
118
|
+
dimOpen: () => styleStack.boldDim.push("dim"),
|
|
119
|
+
boldDimClose: () => styleStack.boldDim.pop(),
|
|
120
|
+
italicOpen: () => {
|
|
121
|
+
styleState.italic = true;
|
|
122
|
+
},
|
|
123
|
+
italicClose: () => {
|
|
124
|
+
styleState.italic = false;
|
|
125
|
+
},
|
|
126
|
+
underlineOpen: () => {
|
|
127
|
+
styleState.underline = true;
|
|
128
|
+
},
|
|
129
|
+
underlineClose: () => {
|
|
130
|
+
styleState.underline = false;
|
|
131
|
+
},
|
|
132
|
+
inverseOpen: () => {
|
|
133
|
+
styleState.inverse = true;
|
|
134
|
+
},
|
|
135
|
+
inverseClose: () => {
|
|
136
|
+
styleState.inverse = false;
|
|
137
|
+
},
|
|
138
|
+
strikethroughOpen: () => {
|
|
139
|
+
styleState.strikethrough = true;
|
|
140
|
+
},
|
|
141
|
+
strikethroughClose: () => {
|
|
142
|
+
styleState.strikethrough = false;
|
|
143
|
+
},
|
|
144
|
+
reset: () => {
|
|
145
|
+
styleState.strikethrough = false;
|
|
146
|
+
styleState.inverse = false;
|
|
147
|
+
styleState.italic = false;
|
|
148
|
+
styleStack.boldDim = [];
|
|
149
|
+
styleStack.backgroundColor = [];
|
|
150
|
+
styleStack.foregroundColor = [];
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let x = 0;
|
|
155
|
+
let y = 0;
|
|
156
|
+
let nAnsi = 0;
|
|
157
|
+
let nPlain = 0;
|
|
158
|
+
|
|
159
|
+
const bundle = (type, value) => {
|
|
160
|
+
const chunk = {
|
|
161
|
+
type,
|
|
162
|
+
value,
|
|
163
|
+
position: {
|
|
164
|
+
x,
|
|
165
|
+
y,
|
|
166
|
+
n: nPlain,
|
|
167
|
+
raw: nAnsi,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (type === "text" || type === "ansi") {
|
|
172
|
+
const style = {};
|
|
173
|
+
|
|
174
|
+
const foregroundColor = getForegroundColor();
|
|
175
|
+
const backgroundColor = getBackgroundColor();
|
|
176
|
+
const dim = getDim();
|
|
177
|
+
const bold = getBold();
|
|
178
|
+
|
|
179
|
+
if (foregroundColor) {
|
|
180
|
+
style.foregroundColor = foregroundColor;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (backgroundColor) {
|
|
184
|
+
style.backgroundColor = backgroundColor;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (dim) {
|
|
188
|
+
style.dim = dim;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (bold) {
|
|
192
|
+
style.bold = bold;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (styleState.italic) {
|
|
196
|
+
style.italic = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (styleState.underline) {
|
|
200
|
+
style.underline = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (styleState.inverse) {
|
|
204
|
+
style.inverse = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (styleState.strikethrough) {
|
|
208
|
+
style.strikethrough = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
chunk.style = style;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return chunk;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const word of words) {
|
|
218
|
+
// Newline character
|
|
219
|
+
if (word === "\n") {
|
|
220
|
+
const chunk = bundle("newline", "\n");
|
|
221
|
+
result.chunks.push(chunk);
|
|
222
|
+
|
|
223
|
+
x = 0;
|
|
224
|
+
y += 1;
|
|
225
|
+
nAnsi += 1;
|
|
226
|
+
nPlain += 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
// Text characters
|
|
230
|
+
if (delimiters.includes(word) === false) {
|
|
231
|
+
const chunk = bundle("text", word);
|
|
232
|
+
result.chunks.push(chunk);
|
|
233
|
+
|
|
234
|
+
x += word.length;
|
|
235
|
+
nAnsi += word.length;
|
|
236
|
+
nPlain += word.length;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// ANSI Escape characters
|
|
240
|
+
const ansiTag = ansiTags[word];
|
|
241
|
+
const decorator = decorators[ansiTag];
|
|
242
|
+
if (decorator) {
|
|
243
|
+
const decoratorEffect = decoratorEffects[decorator];
|
|
244
|
+
if (decoratorEffect) {
|
|
245
|
+
decoratorEffect(ansiTag);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const chunk = bundle("ansi", {
|
|
249
|
+
tag: ansiTag,
|
|
250
|
+
ansi: word,
|
|
251
|
+
decorator,
|
|
252
|
+
});
|
|
253
|
+
result.chunks.push(chunk);
|
|
254
|
+
|
|
255
|
+
nAnsi += word.length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const ansiTags = {
|
|
262
|
+
"\u001B[30m": "black",
|
|
263
|
+
"\u001B[31m": "red",
|
|
264
|
+
"\u001B[32m": "green",
|
|
265
|
+
"\u001B[33m": "yellow",
|
|
266
|
+
"\u001B[34m": "blue",
|
|
267
|
+
"\u001B[35m": "magenta",
|
|
268
|
+
"\u001B[36m": "cyan",
|
|
269
|
+
"\u001B[37m": "white",
|
|
270
|
+
|
|
271
|
+
"\u001B[90m": "gray",
|
|
272
|
+
"\u001B[91m": "redBright",
|
|
273
|
+
"\u001B[92m": "greenBright",
|
|
274
|
+
"\u001B[93m": "yellowBright",
|
|
275
|
+
"\u001B[94m": "blueBright",
|
|
276
|
+
"\u001B[95m": "magentaBright",
|
|
277
|
+
"\u001B[96m": "cyanBright",
|
|
278
|
+
"\u001B[97m": "whiteBright",
|
|
279
|
+
|
|
280
|
+
"\u001B[39m": "foregroundColorClose",
|
|
281
|
+
|
|
282
|
+
"\u001B[40m": "bgBlack",
|
|
283
|
+
"\u001B[41m": "bgRed",
|
|
284
|
+
"\u001B[42m": "bgGreen",
|
|
285
|
+
"\u001B[43m": "bgYellow",
|
|
286
|
+
"\u001B[44m": "bgBlue",
|
|
287
|
+
"\u001B[45m": "bgMagenta",
|
|
288
|
+
"\u001B[46m": "bgCyan",
|
|
289
|
+
"\u001B[47m": "bgWhite",
|
|
290
|
+
|
|
291
|
+
"\u001B[100m": "bgGray",
|
|
292
|
+
"\u001B[101m": "bgRedBright",
|
|
293
|
+
"\u001B[102m": "bgGreenBright",
|
|
294
|
+
"\u001B[103m": "bgYellowBright",
|
|
295
|
+
"\u001B[104m": "bgBlueBright",
|
|
296
|
+
"\u001B[105m": "bgMagentaBright",
|
|
297
|
+
"\u001B[106m": "bgCyanBright",
|
|
298
|
+
"\u001B[107m": "bgWhiteBright",
|
|
299
|
+
|
|
300
|
+
"\u001B[49m": "backgroundColorClose",
|
|
301
|
+
|
|
302
|
+
"\u001B[1m": "boldOpen",
|
|
303
|
+
"\u001B[2m": "dimOpen",
|
|
304
|
+
"\u001B[3m": "italicOpen",
|
|
305
|
+
"\u001B[4m": "underlineOpen",
|
|
306
|
+
"\u001B[7m": "inverseOpen",
|
|
307
|
+
"\u001B[8m": "hiddenOpen",
|
|
308
|
+
"\u001B[9m": "strikethroughOpen",
|
|
309
|
+
|
|
310
|
+
"\u001B[22m": "boldDimClose",
|
|
311
|
+
"\u001B[23m": "italicClose",
|
|
312
|
+
"\u001B[24m": "underlineClose",
|
|
313
|
+
"\u001B[27m": "inverseClose",
|
|
314
|
+
"\u001B[28m": "hiddenClose",
|
|
315
|
+
"\u001B[29m": "strikethroughClose",
|
|
316
|
+
|
|
317
|
+
"\u001B[0m": "reset",
|
|
318
|
+
};
|
|
319
|
+
const decorators = {
|
|
320
|
+
black: "foregroundColorOpen",
|
|
321
|
+
red: "foregroundColorOpen",
|
|
322
|
+
green: "foregroundColorOpen",
|
|
323
|
+
yellow: "foregroundColorOpen",
|
|
324
|
+
blue: "foregroundColorOpen",
|
|
325
|
+
magenta: "foregroundColorOpen",
|
|
326
|
+
cyan: "foregroundColorOpen",
|
|
327
|
+
white: "foregroundColorOpen",
|
|
328
|
+
|
|
329
|
+
gray: "foregroundColorOpen",
|
|
330
|
+
redBright: "foregroundColorOpen",
|
|
331
|
+
greenBright: "foregroundColorOpen",
|
|
332
|
+
yellowBright: "foregroundColorOpen",
|
|
333
|
+
blueBright: "foregroundColorOpen",
|
|
334
|
+
magentaBright: "foregroundColorOpen",
|
|
335
|
+
cyanBright: "foregroundColorOpen",
|
|
336
|
+
whiteBright: "foregroundColorOpen",
|
|
337
|
+
|
|
338
|
+
bgBlack: "backgroundColorOpen",
|
|
339
|
+
bgRed: "backgroundColorOpen",
|
|
340
|
+
bgGreen: "backgroundColorOpen",
|
|
341
|
+
bgYellow: "backgroundColorOpen",
|
|
342
|
+
bgBlue: "backgroundColorOpen",
|
|
343
|
+
bgMagenta: "backgroundColorOpen",
|
|
344
|
+
bgCyan: "backgroundColorOpen",
|
|
345
|
+
bgWhite: "backgroundColorOpen",
|
|
346
|
+
|
|
347
|
+
bgGray: "backgroundColorOpen",
|
|
348
|
+
bgRedBright: "backgroundColorOpen",
|
|
349
|
+
bgGreenBright: "backgroundColorOpen",
|
|
350
|
+
bgYellowBright: "backgroundColorOpen",
|
|
351
|
+
bgBlueBright: "backgroundColorOpen",
|
|
352
|
+
bgMagentaBright: "backgroundColorOpen",
|
|
353
|
+
bgCyanBright: "backgroundColorOpen",
|
|
354
|
+
bgWhiteBright: "backgroundColorOpen",
|
|
355
|
+
|
|
356
|
+
foregroundColorClose: "foregroundColorClose",
|
|
357
|
+
backgroundColorClose: "backgroundColorClose",
|
|
358
|
+
|
|
359
|
+
boldOpen: "boldOpen",
|
|
360
|
+
dimOpen: "dimOpen",
|
|
361
|
+
italicOpen: "italicOpen",
|
|
362
|
+
underlineOpen: "underlineOpen",
|
|
363
|
+
inverseOpen: "inverseOpen",
|
|
364
|
+
hiddenOpen: "hiddenOpen",
|
|
365
|
+
strikethroughOpen: "strikethroughOpen",
|
|
366
|
+
|
|
367
|
+
boldDimClose: "boldDimClose",
|
|
368
|
+
italicClose: "italicClose",
|
|
369
|
+
underlineClose: "underlineClose",
|
|
370
|
+
inverseClose: "inverseClose",
|
|
371
|
+
hiddenClose: "hiddenClose",
|
|
372
|
+
strikethroughClose: "strikethroughClose",
|
|
373
|
+
|
|
374
|
+
reset: "reset",
|
|
375
|
+
};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import prettier from "prettier";
|
|
2
|
+
import { parseAnsi } from "./parse_ansi.js";
|
|
3
|
+
import { createSvgRootNode } from "./xml_generator.js";
|
|
4
|
+
|
|
5
|
+
const colorsDefault = {
|
|
6
|
+
black: "#000000",
|
|
7
|
+
red: "#B22222",
|
|
8
|
+
green: "#32CD32",
|
|
9
|
+
yellow: "#DAA520",
|
|
10
|
+
blue: "#4169E1",
|
|
11
|
+
magenta: "#9932CC",
|
|
12
|
+
cyan: "#008B8B",
|
|
13
|
+
white: "#D3D3D3",
|
|
14
|
+
gray: "#A9A9A9",
|
|
15
|
+
redBright: "#FF4500",
|
|
16
|
+
greenBright: "#ADFF2F",
|
|
17
|
+
yellowBright: "#FFFF00",
|
|
18
|
+
blueBright: "#87CEEB",
|
|
19
|
+
magentaBright: "#FF00FF",
|
|
20
|
+
cyanBright: "#00FFFF",
|
|
21
|
+
whiteBright: "#FFFFFF",
|
|
22
|
+
bgBlack: "#000000",
|
|
23
|
+
bgRed: "#B22222",
|
|
24
|
+
bgGreen: "#32CD32",
|
|
25
|
+
bgYellow: "#DAA520",
|
|
26
|
+
bgBlue: "#4169E1",
|
|
27
|
+
bgMagenta: "#9932CC",
|
|
28
|
+
bgCyan: "#008B8B",
|
|
29
|
+
bgWhite: "#D3D3D3",
|
|
30
|
+
bgGray: "#A9A9A9",
|
|
31
|
+
bgRedBright: "#FF0000",
|
|
32
|
+
bgGreenBright: "#ADFF2F",
|
|
33
|
+
bgYellowBright: "#FFFF00",
|
|
34
|
+
bgBlueBright: "#87CEEB",
|
|
35
|
+
bgMagentaBright: "#FF00FF",
|
|
36
|
+
bgCyanBright: "#00FFFF",
|
|
37
|
+
bgWhiteBright: "#FFFFFF",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const renderTerminalSvg = async (
|
|
41
|
+
ansi,
|
|
42
|
+
{
|
|
43
|
+
title = "ansi to terminal",
|
|
44
|
+
// Font: (use monospace fonts for best results)
|
|
45
|
+
fontFamily = "SauceCodePro Nerd Font, Source Code Pro, Courier",
|
|
46
|
+
fontFace,
|
|
47
|
+
// Pixel Values:
|
|
48
|
+
fontSize = 14,
|
|
49
|
+
lineHeight = 18,
|
|
50
|
+
paddingTop = 0,
|
|
51
|
+
paddingLeft = 10,
|
|
52
|
+
paddingBottom = 0,
|
|
53
|
+
paddingRight = 10,
|
|
54
|
+
|
|
55
|
+
backgroundColor = "#282c34",
|
|
56
|
+
foregroundColor = "#abb2bf",
|
|
57
|
+
colors = colorsDefault,
|
|
58
|
+
|
|
59
|
+
// by default: fixed width of 640 + fluid height
|
|
60
|
+
width = 640,
|
|
61
|
+
height,
|
|
62
|
+
maxWidth,
|
|
63
|
+
maxHeight,
|
|
64
|
+
} = {},
|
|
65
|
+
) => {
|
|
66
|
+
const { rows, columns, chunks } = parseAnsi(ansi);
|
|
67
|
+
const font = {
|
|
68
|
+
size: fontSize,
|
|
69
|
+
width: 8.4013671875,
|
|
70
|
+
height: 14,
|
|
71
|
+
face: fontFace,
|
|
72
|
+
family: fontFamily,
|
|
73
|
+
lineHeight,
|
|
74
|
+
emHeightAscent: 10.5546875,
|
|
75
|
+
emHeightDescent: 3.4453125,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const headerHeight = 40;
|
|
79
|
+
const textWidth = columns * font.width;
|
|
80
|
+
const textHeight = rows * (font.lineHeight + 1) + font.emHeightDescent;
|
|
81
|
+
const bodyContentWidth = paddingLeft + textWidth + paddingRight;
|
|
82
|
+
const bodyContentHeight = paddingTop + textHeight + paddingBottom;
|
|
83
|
+
const contentWidth = bodyContentWidth;
|
|
84
|
+
const contentHeight = headerHeight + bodyContentHeight;
|
|
85
|
+
|
|
86
|
+
let computedWidth;
|
|
87
|
+
if (typeof width === "number") {
|
|
88
|
+
computedWidth = width;
|
|
89
|
+
} else {
|
|
90
|
+
computedWidth = contentWidth;
|
|
91
|
+
}
|
|
92
|
+
if (typeof maxWidth === "number" && computedWidth > maxWidth) {
|
|
93
|
+
computedWidth = maxWidth;
|
|
94
|
+
}
|
|
95
|
+
let computedHeight;
|
|
96
|
+
if (typeof height === "number") {
|
|
97
|
+
computedHeight = height;
|
|
98
|
+
} else {
|
|
99
|
+
computedHeight = contentHeight;
|
|
100
|
+
}
|
|
101
|
+
if (typeof maxHeight === "number" && computedHeight > maxHeight) {
|
|
102
|
+
computedHeight = maxHeight;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const svg = createSvgRootNode({
|
|
106
|
+
"xmlns": "http://www.w3.org/2000/svg",
|
|
107
|
+
"font-family": font.family,
|
|
108
|
+
"font-size": font.size,
|
|
109
|
+
"width": computedWidth,
|
|
110
|
+
"height": computedHeight,
|
|
111
|
+
backgroundColor,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
background: {
|
|
115
|
+
const backgroundGroup = svg.createNode("g", {
|
|
116
|
+
id: "background",
|
|
117
|
+
});
|
|
118
|
+
const backgroundRect = svg.createNode("rect", {
|
|
119
|
+
"x": 1,
|
|
120
|
+
"y": 1,
|
|
121
|
+
"width": computedWidth - 2,
|
|
122
|
+
"height": computedHeight - 2,
|
|
123
|
+
"fill": backgroundColor,
|
|
124
|
+
"stroke": "rgba(255,255,255,0.35)",
|
|
125
|
+
"stroke-width": 1,
|
|
126
|
+
"rx": 8,
|
|
127
|
+
});
|
|
128
|
+
backgroundGroup.appendChild(backgroundRect);
|
|
129
|
+
svg.appendChild(backgroundGroup);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
header: {
|
|
133
|
+
const headerGroup = svg.createNode("g", {
|
|
134
|
+
id: "header",
|
|
135
|
+
});
|
|
136
|
+
const iconsGroup = svg.createNode("g", {
|
|
137
|
+
transform: `translate(20,${headerHeight / 2})`,
|
|
138
|
+
});
|
|
139
|
+
const circleA = svg.createNode("circle", {
|
|
140
|
+
cx: 0,
|
|
141
|
+
cy: 0,
|
|
142
|
+
r: 6,
|
|
143
|
+
fill: "#ff5f57",
|
|
144
|
+
});
|
|
145
|
+
iconsGroup.appendChild(circleA);
|
|
146
|
+
const circleB = svg.createNode("circle", {
|
|
147
|
+
cx: 20,
|
|
148
|
+
cy: 0,
|
|
149
|
+
r: 6,
|
|
150
|
+
fill: "#febc2e",
|
|
151
|
+
});
|
|
152
|
+
iconsGroup.appendChild(circleB);
|
|
153
|
+
const circleC = svg.createNode("circle", {
|
|
154
|
+
cx: 40,
|
|
155
|
+
cy: 0,
|
|
156
|
+
r: 6,
|
|
157
|
+
fill: "#28c840",
|
|
158
|
+
});
|
|
159
|
+
iconsGroup.appendChild(circleC);
|
|
160
|
+
headerGroup.appendChild(iconsGroup);
|
|
161
|
+
|
|
162
|
+
const text = svg.createNode("text", {
|
|
163
|
+
"fill": "#abb2bf",
|
|
164
|
+
"text-anchor": "middle",
|
|
165
|
+
"x": computedWidth / 2,
|
|
166
|
+
"y": headerHeight / 2,
|
|
167
|
+
});
|
|
168
|
+
text.setContent(title);
|
|
169
|
+
headerGroup.appendChild(text);
|
|
170
|
+
svg.appendChild(headerGroup);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
body: {
|
|
174
|
+
const bodyComputedHeight = computedHeight - headerHeight + paddingBottom;
|
|
175
|
+
const foreignObject = svg.createNode("foreignObject", {
|
|
176
|
+
id: "body",
|
|
177
|
+
y: headerHeight,
|
|
178
|
+
width: "100%",
|
|
179
|
+
height: bodyComputedHeight,
|
|
180
|
+
// we can't really know in advance the size of the scrollbar
|
|
181
|
+
// so putting overflow: "auto"
|
|
182
|
+
// is not that great as it would create a scrollbar
|
|
183
|
+
// in both axes when a single one is required
|
|
184
|
+
overflow: "hidden",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
svg.appendChild(foreignObject);
|
|
188
|
+
const bodySvg = svg.createNode("svg", {
|
|
189
|
+
"width": bodyContentWidth,
|
|
190
|
+
"height": bodyContentHeight,
|
|
191
|
+
"font-family": "monospace",
|
|
192
|
+
"font-variant-east-asian": "full-width",
|
|
193
|
+
"fill": foregroundColor,
|
|
194
|
+
});
|
|
195
|
+
foreignObject.appendChild(bodySvg);
|
|
196
|
+
|
|
197
|
+
const offsetLeft = paddingLeft;
|
|
198
|
+
const offsetTop = paddingTop + font.lineHeight - font.emHeightDescent;
|
|
199
|
+
const textContainer = svg.createNode("g", {
|
|
200
|
+
// transform: `translate(${paddingLeft}, ${offsetTop})`,
|
|
201
|
+
});
|
|
202
|
+
bodySvg.appendChild(textContainer);
|
|
203
|
+
|
|
204
|
+
for (const chunk of chunks) {
|
|
205
|
+
const { type, value, style } = chunk;
|
|
206
|
+
if (type !== "text") {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { position } = chunk;
|
|
211
|
+
const x =
|
|
212
|
+
offsetLeft + adjustXforWhitespace(value, position.x) * font.width;
|
|
213
|
+
const y = offsetTop + position.y + font.lineHeight * position.y;
|
|
214
|
+
const w = font.width * value.length;
|
|
215
|
+
const attrs = {};
|
|
216
|
+
if (style.bold) {
|
|
217
|
+
attrs["font-weight"] = "bold";
|
|
218
|
+
}
|
|
219
|
+
if (style.italic) {
|
|
220
|
+
attrs["font-style"] = "italic";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let opacity = 1;
|
|
224
|
+
if (style.dim) {
|
|
225
|
+
opacity = 0.5;
|
|
226
|
+
}
|
|
227
|
+
if (style.backgroundColor) {
|
|
228
|
+
const backgroundColor = colors[style.backgroundColor];
|
|
229
|
+
const rect = svg.createNode("rect", {
|
|
230
|
+
x,
|
|
231
|
+
y: y - font.lineHeight + font.emHeightDescent,
|
|
232
|
+
width: w,
|
|
233
|
+
height: font.lineHeight + 1,
|
|
234
|
+
fill: backgroundColor,
|
|
235
|
+
...(opacity ? { opacity } : {}),
|
|
236
|
+
});
|
|
237
|
+
textContainer.appendChild(rect);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let textForegroundColor;
|
|
241
|
+
if (style.foregroundColor) {
|
|
242
|
+
textForegroundColor = colors[style.foregroundColor];
|
|
243
|
+
attrs["fill"] = textForegroundColor;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Underline & Strikethrough:
|
|
247
|
+
// Some SVG implmentations do not support underline and
|
|
248
|
+
// strikethrough for <text> elements (see Sketch 49.2)
|
|
249
|
+
if (style.underline) {
|
|
250
|
+
const yOffset = font.height * 0.14;
|
|
251
|
+
const ys = y - -yOffset;
|
|
252
|
+
const xw = x + w;
|
|
253
|
+
const path = svg.createNode("path", {
|
|
254
|
+
d: `M${x},${ys} L${xw},${ys} Z`,
|
|
255
|
+
stroke: textForegroundColor || foregroundColor,
|
|
256
|
+
});
|
|
257
|
+
textContainer.appendChild(path);
|
|
258
|
+
}
|
|
259
|
+
if (style.strikethrough) {
|
|
260
|
+
const yOffset = font.height * 0.3;
|
|
261
|
+
const ys = y - yOffset;
|
|
262
|
+
const xw = x + w;
|
|
263
|
+
const path = svg.createNode("path", {
|
|
264
|
+
d: `M${x},${ys} L${xw},${ys} Z`,
|
|
265
|
+
stroke: textForegroundColor || foregroundColor,
|
|
266
|
+
});
|
|
267
|
+
textContainer.appendChild(path);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Do not output elements containing whitespace with no style
|
|
271
|
+
if (
|
|
272
|
+
value.replace(/ /g, "").length === 0 &&
|
|
273
|
+
Object.keys(attrs).length === 0
|
|
274
|
+
) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const text = svg.createNode("text", {
|
|
279
|
+
x,
|
|
280
|
+
y,
|
|
281
|
+
...attrs,
|
|
282
|
+
});
|
|
283
|
+
text.setContent(value);
|
|
284
|
+
textContainer.appendChild(text);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const svgString = svg.renderAsString();
|
|
289
|
+
const formatted = await prettier.format(svgString, {
|
|
290
|
+
parser: "html",
|
|
291
|
+
});
|
|
292
|
+
return formatted;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Some SVG Implementations drop whitespaces
|
|
296
|
+
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:space
|
|
297
|
+
const adjustXforWhitespace = (text, x) => {
|
|
298
|
+
const leadingSpace = text.match(/^\s*/g);
|
|
299
|
+
return x + leadingSpace[0].length;
|
|
300
|
+
};
|