A lib to handle operation on ANSI text
npm install @macfja/ansi
# or
pnpm add --save @macfja/ansi
# or
yarn add --save @macfja/ansi
Parse a string and return a list of text or ansi sequence.
/**
* Parse a string into a series of Text and Ansi sequence
* @param text
*/
declare function parse(text: string): ParsedText;
Transform a list of sequence to a ANSI text (reverse of parse
)
/**
* Stringify a series of Text and Ansi sequence into a string
* @param input
*/
declare function stringify(input: ParsedText): string;
Insert a string inside an ansi text at a visual position.
The inserted string is isolated from the ansi text (ANSI instruction are stop before the inserted text, and restarted after it).
/**
* Insert a text into an Ansi text at a visible (printable) position
* @param text The text to insert into
* @param visiblePosition The visible (printable) position where to insert the text
* @param value The text to insert
*/
declare function insertAt(text: string | ParsedText, visiblePosition: number, value: string): string;
Wrap an ANSI text at a defined size.
Can be a hard wrap or a soft wrap (if it can, it won't break word)
/**
* Wrap an Ansi text within a defined length
* @param text The text to wrap
* @param cols The maximum width of the text
* @param options
*/
declare function wrap(text: string, cols: number, options?: WrapOptions): string;
type WrapOptions = {
/**
* Indicate what to do with white space.
* - "trim", will remove any white space at start and end of each line
* - "fill", will remove any white space at the start of line and add space to match the col size
* - "preserve", will leave line as-is
*/
whiteSpace: "trim" | "fill" | "preserve";
/**
* Indicate how to wrap line.
* - "word", if possible it won't break any word (soft wrap)
* - "char", break line at the desired col without considering word (hard wrap)
*/
break: "word" | "char";
};
Remove all ANSI instruction from a text
/**
* Remove all Ansi Escape Code from a text
* @param text
*/
declare function stripAnsi(text: string | ParsedText): string;
Return the ANSI position from a visual position
/**
* Return the ANSI position from a visual position
* @param text The ANSI text to search in
* @param visiblePosition The visible (printable) position
*/
declare function ansiPosition(text: string | ParsedText, visiblePosition: number): number;
Truncate (no wrapping) an ANSI text at a defined size.
Text can be truncate at the start, the end or in the middle
/**
* Limit the length of an Ansi text by truncating it if needed
* @param text The text to truncate
* @param cols The maximum width of the text
* @param position Where to truncate [default: "end"]
*/
declare function truncate(text: string, cols: number, position?: "start" | "middle" | "end"): string;
You can extend the behavior of this lib with extensions:
The library come with many standard ANSI escape code:
Code | Name |
---|---|
CUU | Cursor Up |
CUD | Cursor Down |
CUF | Cursor Forward |
CUB | Cursor Back |
CNL | Cursor Next Line |
CPL | Cursor Previous Line |
CHA | Cursor Horizontal Absolute |
ED | Erase in Display |
EL | Erase in Line |
SU | Scroll Up |
SD | Scroll Down |
CUP | Cursor Position |
HVP | Horizontal Vertical Position |
AUX On | |
AUX Off | |
DSR | Device Status Report |
SGR | Select Graphic Rendition |
OSC (link) | Operating System Command (Hypertext Link) |
But there are many more code (standard and non-standard).
To add the capacity to correctly parse them, you can add new ANSI code matcher:
Example: Implementation of the "Disable reporting focus" code
import { registerAnsiMatcher, AnsiMatcher, FE_ESCAPE } from "@macfja/ansi/extension"
import regexpEscape from "regexp.escape";
const noReportFocus = new AnsiMatcher(
// The Ansi Escape code category
FE_ESCAPE.CSI,
// The regular expression that match the escape code (the match '0' will be used)
new RegExp(regexpEscape(`${FE_ESCAPE.CSI}?1004l`)),
// The non dynamic part of the escape code (use to quickly search in text)
`${FE_ESCAPE.CSI}?1004l`
)
registerAnsiMatcher(noReportFocus)
Sometimes an ANSI text can be optimized, for example with SGR multiple instruction can be grouped together (like foreground color, background color, etc.). To do those optimisation, a postprecessor can be applied after the parsing of the text:
Example: remplacing matching 24bit color with 3/4 bit color
import { type postprocess, registerPostprocess, recalculatePosition, type ParsedText, AnsiSequence, FE_ESCAPE } from "@macfja/ansi/extension"
const colorPostprocessor: postprocess = (input: ParsedText) => {
// VGA Color
// https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
const colorMap = {
0: [0,0,0],
1: [170,0,0],
2: [0,170,0],
3: [170,85,0],
4: [0,0,170],
5: [170,0,170],
6: [0,170,170],
7: [170,170,170],
}
return recalculatePosition(input.map(item => {
if (
!(item instanceof AnsiSequence)
|| item.kind !== FE_ESCAPE.CSI
|| !item.sequence.endsWith('m')
|| !item.sequence.startsWith(`${FE_ESCAPE.CSI}38;2;`)
|| !item.sequence.startsWith(`${FE_ESCAPE.CSI}48;2;`)
) {
return item;
}
const color = item.sequence.match(/(?<type>[34]8;2;(?<r>\d+);(?<g>\d+);(?<b>\d+)m$/)
if (color.groups?.type === undefined || color.groups?.r === undefined || color.groups?.g === undefined || color.groups?.b === undefined) {
return item;
}
const colorCode = colorMap.findIndex(codes => codes.join('-') === `${color.groups.r}-${color.groups.g}-${color.groups.b}`)
if (colorCode === undefined) {
return item
}
return new AnsiSequence(FE_ESCAPE.CSI, `${FE_ESCAPE.CSI}${color.groups.type}${colorCode}m`, item.start)
}))
}
registerPostprocess(colorPostprocessor)
When doing some operation of ANSI text (inserting a char, wrapping lines, etc.), we need to close any ANSI code that is still affect the rendering, and reopen everything after. To do so, there are 2 functions (one for closing code, one for reopening them)
declare function registerToClose(fn: toClose): void;
declare function registerToReopen(fn: toReopen): void;
type toClose = (input: ParsedText, offset?: number) => Array<AnsiSequence>;
type toReopen = (input: ParsedText, offset?: number) => Array<AnsiSequence>;
The decorate package come with a function to decorate a text:
declare function encapsulate(input: string, options: EncapsulateOption): string;
The function second parameter is an object describing how to decorate the text:
type EncapsulateOption = {
/**
* Set to true if the decorator add char before the first line of the input text
*/
beforeLine?: boolean;
/**
* Set to true if the decorator need add char(s) before each line
*/
beforeCol?: boolean;
/**
* Set to true if the decorator add char after the last line of the input text
*/
afterLine?: boolean;
/**
* Set to true if the decorator need add char(s) after each line
*/
afterCol?: boolean;
/**
* The decorator function will be call multiple times:
* - With `line` === -1, and `col` from 0 (or -1) to `cols` - 1 (or `cols`) if `beforeLine` is `true`
* - With `line` === `lines`, and `col` from 0 (or -1) to `cols` - 1 (or `cols`) if `afterLine` is `true`
* - With `col` === -1, and `line` from 0 (or -1) to `lines` - 1 (or `lines`) if `beforeCol` is `true`
* - With `col` === `cols`, and `line` from 0 (or -1) to `lines` - 1 (or `lines`) if `afterCol` is `true`
* @param line
* @param col
* @param lines
* @param cols
*/
decorator: (line: number, col: number, lines: number, cols: number) => string;
};
Several pre-made EncapsulateOption are available:
ASCIIBox
: Create a box with+
,-
and|
roundedBox
: Create a box with a continuous line with rounded borderdoubleSquareBox
: : Create a box with a continuous double linesquareBox
: : Create a box with a continuous linecurlyBracket
: Prefix the text with a big curly bracketpadding()
: A function to create a padding/margin space around the textboxChar()
: A function to create a box
Examples
roundedBox
import { encapsulate, squareBox } from "@macfja/ansi/decorate"
console.log(encapsulate(' Lorem ipsum dolor sit amet, \n consectetur adipiscing elit. ', roundedBox))
╭──────────────────────────────╮
│ Lorem ipsum dolor sit amet, │
│ consectetur adipiscing elit. │
╰──────────────────────────────╯
curlyBracket
import { encapsulate, curlyBracket } from "@macfja/ansi/decorate"
console.log(encapsulate('Lorem ipsum\ndolor sit\namet,\nconsectetur\nadipiscing elit.', curlyBracket))
⎧ Lorem ipsum
⎪ dolor sit
⎨ amet,
⎪ consectetur
⎩ adipiscing elit.