UNPKG

console-toolkit

Version:

Toolkit to produce a fancy console output (boxes, tables, charts, colors).

399 lines (365 loc) 10.8 kB
// Support for states based on SGR commands. See https://en.wikipedia.org/wiki/ANSI_escape_code for more details. import { Commands, ColorFormatSize, isFgColorCommand, isBgColorCommand, isFontCommand, setCommands, matchSgr } from './sgr.js'; export const RESET_STATE = { bold: null, dim: null, italic: null, underline: null, blink: null, inverse: null, hidden: null, strikethrough: null, overline: null, foreground: null, background: null, decoration: null, font: null }; const defaultState = Symbol('defaultState'); let toState; export const extractState = (s, initState = defaultState) => { let state = toState(initState); matchSgr.lastIndex = 0; for (const match of s.matchAll(matchSgr)) state = addCommandsToState(state, match[1].split(';')); return state; }; toState = value => { switch (typeof value) { case 'object': if (!value) return RESET_STATE; if (typeof value.getState == 'function') return value.getState(); return value; case 'string': if (!value) break; return extractState(value); } return {}; }; export {toState}; const TOTAL_RESETS = Array.from(Object.keys(RESET_STATE)).length; const getStateResets = state => { let resetCount = 0; for (const name of Object.keys(RESET_STATE)) { if (state[name] === null) ++resetCount; } return resetCount; }; export const combineStates = (...states) => { let state = {}; for (const s of states) { const currentState = toState(s); for (const [name, value] of Object.entries(currentState)) { if (value !== undefined) state[name] = value; } } return state; }; export const commandsToState = commands => { let state = {}; for (let i = 0; i < commands.length; ++i) { const currentCommand = commands[i]; switch (currentCommand) { case '': // reset case Commands.RESET_ALL: state = RESET_STATE; continue; case Commands.BOLD: state.bold = currentCommand; continue; case Commands.DIM: state.dim = currentCommand; continue; case Commands.ITALIC: state.italic = currentCommand; continue; case Commands.UNDERLINE: case Commands.DOUBLE_UNDERLINE: case Commands.CURLY_UNDERLINE: state.underline = currentCommand; continue; case Commands.BLINK: case Commands.RAPID_BLINK: state.blink = currentCommand; continue; case Commands.INVERSE: state.inverse = currentCommand; continue; case Commands.HIDDEN: state.hidden = currentCommand; continue; case Commands.STRIKETHROUGH: state.strikethrough = currentCommand; continue; case Commands.OVERLINE: state.overline = currentCommand; continue; case Commands.RESET_BOLD: // case Commands.RESET_DIM: state.bold = state.dim = null; continue; case Commands.RESET_ITALIC: state.italic = null; continue; case Commands.RESET_UNDERLINE: // case Commands.RESET_DOUBLE_UNDERLINE: // case Commands.RESET_CURLY_UNDERLINE: state.underline = null; continue; case Commands.RESET_BLINK: // case RESET_RAPID_BLINK: state.blink = null; continue; case Commands.RESET_INVERSE: state.inverse = null; continue; case Commands.RESET_HIDDEN: state.hidden = null; continue; case Commands.RESET_STRIKETHROUGH: state.strikethrough = null; continue; case Commands.RESET_OVERLINE: state.overline = null; continue; case Commands.RESET_DECORATION_COLOR: state.decoration = null; continue; case Commands.EXTENDED_COLOR: { const next = ColorFormatSize[commands[i + 1]], color = commands.slice(i, i + next); i += next - 1; state.foreground = color; continue; } case Commands.BG_EXTENDED_COLOR: { const next = ColorFormatSize[commands[i + 1]], color = commands.slice(i, i + next); i += next - 1; state.background = color; continue; } case Commands.DECORATION_COLOR: { const next = ColorFormatSize[commands[i + 1]], color = commands.slice(i, i + next); i += next - 1; state.decoration = color; continue; } case Commands.DEFAULT_COLOR: state.foreground = null; continue; case Commands.BG_DEFAULT_COLOR: state.background = null; continue; case Commands.DEFAULT_FONT: state.font = null; continue; } if (isFgColorCommand(currentCommand)) { state.foreground = currentCommand; continue; } if (isBgColorCommand(currentCommand)) { state.background = currentCommand; continue; } if (isFontCommand(currentCommand)) { state.font = currentCommand; continue; } } return state; }; export const addCommandsToState = (state, commands) => combineStates(state, commandsToState(commands)); const equalColors = (a, b) => { if (a === b) return true; if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((value, index) => value === b[index]); return false; }; const pushColor = (commands, color) => { if (Array.isArray(color)) { commands.push(...color); } else { commands.push(color); } return commands; }; const resetColorProperties = { foreground: Commands.RESET_COLOR, background: Commands.RESET_BG_COLOR, decoration: Commands.RESET_DECORATION_COLOR }; const chainedStates = {bold: 1, dim: 1}; export const stateToCommands = state => { state = toState(state); const commands = []; let resetCount = 0; // process chained states separately if (state.bold === null) { commands.push(Commands.RESET_BOLD); ++resetCount; } if (state.dim === null) { commands.push(Commands.RESET_DIM); ++resetCount; } state.bold && commands.push(state.bold); state.dim && commands.push(state.dim); for (const [name, value] of Object.entries(state)) { if (chainedStates[name] === 1) continue; // skip chained states if (resetColorProperties.hasOwnProperty(name)) { // colors if (value === null) { commands.push(resetColorProperties[name]); ++resetCount; continue; } value && pushColor(commands, value); continue; } if (value === null) { commands.push(Commands['RESET_' + name.toUpperCase()]); ++resetCount; continue; } value && commands.push(value); } return resetCount === TOTAL_RESETS ? [''] : commands; }; export const stateTransition = (prev, next) => { prev = toState(prev); next = toState(next); const commands = []; let resetCount = 0; // process chained states separately if (prev.bold !== next.bold) { if (next.bold === null) { commands.push(Commands.RESET_BOLD); } else if (next.bold) { if (next.dim === null && prev.dim !== null) commands.push(Commands.RESET_DIM); commands.push(next.bold); } next.dim && commands.push(next.dim); } else { if (prev.dim !== next.dim) { if (next.dim === null) { commands.push(Commands.RESET_DIM); next.bold && commands.push(next.bold); } else { next.dim && commands.push(next.dim); } } } if (next.bold === null) ++resetCount; if (next.dim === null) ++resetCount; for (const name of Object.keys(RESET_STATE)) { if (chainedStates[name] === 1) continue; // skip chained states const value = next[name]; if (resetColorProperties.hasOwnProperty(name)) { // color if (value === null) { if (prev[name] !== null) commands.push(resetColorProperties[name]); ++resetCount; continue; } if (value) { if (!equalColors(prev[name], value)) pushColor(commands, value); } continue; } if (value === null) { if (prev[name] !== null) commands.push(Commands['RESET_' + name.toUpperCase()]); ++resetCount; continue; } if (value) { if (prev[name] !== value) commands.push(value); } } if (resetCount === TOTAL_RESETS) { const prevResets = getStateResets(prev); return prevResets === TOTAL_RESETS ? [] : ['']; } return commands; }; export const stateReverseTransition = (prev, next) => { prev = toState(prev); next = toState(next); const commands = []; let resetCount = 0; // process chained states separately if (next.bold !== prev.bold) { if (!prev.bold) { commands.push(Commands.RESET_BOLD); } else { if (!prev.dim && next.dim) commands.push(Commands.RESET_DIM); commands.push(prev.bold); } prev.dim && commands.push(prev.dim); } else { if (next.dim !== prev.dim) { if (!prev.dim) { commands.push(Commands.RESET_DIM); prev.bold && commands.push(prev.bold); } else { prev.dim && commands.push(prev.dim); } } } if (prev.bold === null) ++resetCount; if (prev.dim === null) ++resetCount; for (const name of Object.keys(RESET_STATE)) { const value = prev[name]; if (resetColorProperties.hasOwnProperty(name)) { // color if (!value) { if (next[name]) commands.push(resetColorProperties[name]); if (value === null) ++resetCount; continue; } if (!equalColors(next[name], value)) pushColor(commands, value); continue; } if (!value) { if (next[name]) commands.push(Commands['RESET_' + name.toUpperCase()]); if (value === null) ++resetCount; continue; } if (next[name] !== value) commands.push(value); } if (resetCount === TOTAL_RESETS) { const nextResets = getStateResets(next); return nextResets === TOTAL_RESETS ? [] : ['']; } return commands; }; export const stringifyCommands = commands => (commands?.length ? setCommands(commands) : ''); export const optimize = (s, initState = defaultState) => { let state = toState(initState), result = '', start = 0; matchSgr.lastIndex = 0; for (const match of s.matchAll(matchSgr)) { if (start < match.index) { const commands = initState !== state ? stateTransition(initState, state) : []; result += stringifyCommands(commands); initState = state; result += s.substring(start, match.index); } state = addCommandsToState(state, match[1].split(';')); start = match.index + match[0].length; } const commands = initState !== state ? stateTransition(initState, state) : []; result += stringifyCommands(commands); if (start < s.length) result += s.substring(start); return result; };