@jsenv/terminal-recorder
Version:
Record terminal output as .svg, .gif, .webm, .mp4
377 lines (338 loc) • 9.59 kB
JavaScript
// inspired by https://github.com/F1LT3R/parse-ansi/blob/master/index.js
import { measureTextWidth } from "@jsenv/terminal-text-size";
import ansiRegex from "ansi-regex";
import stripAnsi from "strip-ansi";
export const parseAnsi = (ansi) => {
ansi = ansi.replace(/\r\n/g, "\n"); // normalize windows line endings
const plainText = stripAnsi(ansi);
const lines = plainText.split("\n");
const rows = lines.length;
let columns = 0;
for (const line of lines) {
const len = line.length;
if (len > columns) {
columns = len;
}
}
const result = {
raw: ansi,
plainText,
rows,
columns,
chunks: [],
};
let words;
const delimiters = [];
{
const matches = ansi.match(ansiRegex()) || [];
for (const match of matches) {
if (!delimiters.includes(match)) {
delimiters.push(match);
}
}
delimiters.push("\n");
const splitString = (str, delimiter) => {
const result = [];
let index = 0;
const parts = str.split(delimiter);
for (const part of parts) {
result.push(part);
if (index < parts.length - 1) {
result.push(delimiter);
}
index++;
}
return result;
};
const splitArray = (array, delimiter) => {
let result = [];
for (const part of array) {
let subRes = splitString(part, delimiter);
subRes = subRes.filter((str) => {
return Boolean(str);
});
result = result.concat(subRes);
}
return result;
};
const superSplit = (splittable, delimiters) => {
if (delimiters.length === 0) {
return splittable;
}
if (typeof splittable === "string") {
const delimiter = delimiters[delimiters.length - 1];
const split = splitString(splittable, delimiter);
return superSplit(split, delimiters.slice(0, -1));
}
if (Array.isArray(splittable)) {
const delimiter = delimiters[delimiters.length - 1];
const split = splitArray(splittable, delimiter);
return superSplit(split, delimiters.slice(0, -1));
}
return false;
};
words = superSplit(ansi, delimiters);
}
const styleStack = {
foregroundColor: [],
backgroundColor: [],
boldDim: [],
};
const getForegroundColor = () => {
if (styleStack.foregroundColor.length > 0) {
return styleStack.foregroundColor[styleStack.foregroundColor.length - 1];
}
return false;
};
const getBackgroundColor = () => {
if (styleStack.backgroundColor.length > 0) {
return styleStack.backgroundColor[styleStack.backgroundColor.length - 1];
}
return false;
};
const getDim = () => {
return styleStack.boldDim.includes("dim");
};
const getBold = () => {
return styleStack.boldDim.includes("bold");
};
const styleState = {
italic: false,
underline: false,
inverse: false,
hidden: false,
strikethrough: false,
};
const decoratorEffects = {
foregroundColorOpen: (color) => styleStack.foregroundColor.push(color),
foregroundColorClose: () => styleStack.foregroundColor.pop(),
backgroundColorOpen: (color) => styleStack.backgroundColor.push(color),
backgroundColorClose: () => styleStack.backgroundColor.pop(),
boldOpen: () => styleStack.boldDim.push("bold"),
dimOpen: () => styleStack.boldDim.push("dim"),
boldDimClose: () => styleStack.boldDim.pop(),
italicOpen: () => {
styleState.italic = true;
},
italicClose: () => {
styleState.italic = false;
},
underlineOpen: () => {
styleState.underline = true;
},
underlineClose: () => {
styleState.underline = false;
},
inverseOpen: () => {
styleState.inverse = true;
},
inverseClose: () => {
styleState.inverse = false;
},
strikethroughOpen: () => {
styleState.strikethrough = true;
},
strikethroughClose: () => {
styleState.strikethrough = false;
},
reset: () => {
styleState.underline = false;
styleState.strikethrough = false;
styleState.inverse = false;
styleState.italic = false;
styleStack.boldDim = [];
styleStack.backgroundColor = [];
styleStack.foregroundColor = [];
},
};
let x = 0;
let y = 0;
let nAnsi = 0;
let nPlain = 0;
const bundle = (type, value, { width = 0, height = 0 } = {}) => {
const chunk = {
type,
value,
position: {
x,
y,
n: nPlain,
raw: nAnsi,
width,
height,
},
};
if (type === "text" || type === "ansi") {
const style = {};
const foregroundColor = getForegroundColor();
const backgroundColor = getBackgroundColor();
const dim = getDim();
const bold = getBold();
if (foregroundColor) {
style.foregroundColor = foregroundColor;
}
if (backgroundColor) {
style.backgroundColor = backgroundColor;
}
if (dim) {
style.dim = dim;
}
if (bold) {
style.bold = bold;
}
if (styleState.italic) {
style.italic = true;
}
if (styleState.underline) {
style.underline = true;
}
if (styleState.inverse) {
style.inverse = true;
}
if (styleState.strikethrough) {
style.strikethrough = true;
}
chunk.style = style;
}
return chunk;
};
for (const word of words) {
// Newline character
if (word === "\n") {
const chunk = bundle("newline", "\n", { height: 1 });
result.chunks.push(chunk);
x = 0;
y += 1;
nAnsi += 1;
nPlain += 1;
continue;
}
// Text characters
if (delimiters.includes(word) === false) {
const width = measureTextWidth(word);
const chunk = bundle("text", word, { width });
result.chunks.push(chunk);
x += width;
nAnsi += width;
nPlain += width;
continue;
}
// ANSI Escape characters
const ansiTag = ansiTags[word];
const decorator = decorators[ansiTag];
if (decorator) {
const decoratorEffect = decoratorEffects[decorator];
if (decoratorEffect) {
decoratorEffect(ansiTag);
}
}
const chunk = bundle("ansi", {
tag: ansiTag,
ansi: word,
decorator,
});
result.chunks.push(chunk);
nAnsi += word.length;
}
return result;
};
const ansiTags = {
"\u001B[30m": "black",
"\u001B[31m": "red",
"\u001B[32m": "green",
"\u001B[33m": "yellow",
"\u001B[34m": "blue",
"\u001B[35m": "magenta",
"\u001B[36m": "cyan",
"\u001B[37m": "white",
"\u001B[90m": "gray",
"\u001B[91m": "redBright",
"\u001B[92m": "greenBright",
"\u001B[93m": "yellowBright",
"\u001B[94m": "blueBright",
"\u001B[95m": "magentaBright",
"\u001B[96m": "cyanBright",
"\u001B[97m": "whiteBright",
"\u001B[39m": "foregroundColorClose",
"\u001B[40m": "bgBlack",
"\u001B[41m": "bgRed",
"\u001B[42m": "bgGreen",
"\u001B[43m": "bgYellow",
"\u001B[44m": "bgBlue",
"\u001B[45m": "bgMagenta",
"\u001B[46m": "bgCyan",
"\u001B[47m": "bgWhite",
"\u001B[100m": "bgGray",
"\u001B[101m": "bgRedBright",
"\u001B[102m": "bgGreenBright",
"\u001B[103m": "bgYellowBright",
"\u001B[104m": "bgBlueBright",
"\u001B[105m": "bgMagentaBright",
"\u001B[106m": "bgCyanBright",
"\u001B[107m": "bgWhiteBright",
"\u001B[49m": "backgroundColorClose",
"\u001B[1m": "boldOpen",
"\u001B[2m": "dimOpen",
"\u001B[3m": "italicOpen",
"\u001B[4m": "underlineOpen",
"\u001B[7m": "inverseOpen",
"\u001B[8m": "hiddenOpen",
"\u001B[9m": "strikethroughOpen",
"\u001B[22m": "boldDimClose",
"\u001B[23m": "italicClose",
"\u001B[24m": "underlineClose",
"\u001B[27m": "inverseClose",
"\u001B[28m": "hiddenClose",
"\u001B[29m": "strikethroughClose",
"\u001B[0m": "reset",
};
const decorators = {
black: "foregroundColorOpen",
red: "foregroundColorOpen",
green: "foregroundColorOpen",
yellow: "foregroundColorOpen",
blue: "foregroundColorOpen",
magenta: "foregroundColorOpen",
cyan: "foregroundColorOpen",
white: "foregroundColorOpen",
gray: "foregroundColorOpen",
redBright: "foregroundColorOpen",
greenBright: "foregroundColorOpen",
yellowBright: "foregroundColorOpen",
blueBright: "foregroundColorOpen",
magentaBright: "foregroundColorOpen",
cyanBright: "foregroundColorOpen",
whiteBright: "foregroundColorOpen",
bgBlack: "backgroundColorOpen",
bgRed: "backgroundColorOpen",
bgGreen: "backgroundColorOpen",
bgYellow: "backgroundColorOpen",
bgBlue: "backgroundColorOpen",
bgMagenta: "backgroundColorOpen",
bgCyan: "backgroundColorOpen",
bgWhite: "backgroundColorOpen",
bgGray: "backgroundColorOpen",
bgRedBright: "backgroundColorOpen",
bgGreenBright: "backgroundColorOpen",
bgYellowBright: "backgroundColorOpen",
bgBlueBright: "backgroundColorOpen",
bgMagentaBright: "backgroundColorOpen",
bgCyanBright: "backgroundColorOpen",
bgWhiteBright: "backgroundColorOpen",
foregroundColorClose: "foregroundColorClose",
backgroundColorClose: "backgroundColorClose",
boldOpen: "boldOpen",
dimOpen: "dimOpen",
italicOpen: "italicOpen",
underlineOpen: "underlineOpen",
inverseOpen: "inverseOpen",
hiddenOpen: "hiddenOpen",
strikethroughOpen: "strikethroughOpen",
boldDimClose: "boldDimClose",
italicClose: "italicClose",
underlineClose: "underlineClose",
inverseClose: "inverseClose",
hiddenClose: "hiddenClose",
strikethroughClose: "strikethroughClose",
reset: "reset",
};