@macfja/ansi v1.0.0
@macfja/ansi
A lib to handle operation on ANSI text
Installation
npm install @macfja/ansi
# or
pnpm add --save @macfja/ansi
# or
yarn add --save @macfja/ansi
API
Main package
parse
function
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;
stringify
function
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;
insertAt
function
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
function
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";
};
stripAnsi
function
Remove all ANSI instruction from a text
/**
* Remove all Ansi Escape Code from a text
* @param text
*/
declare function stripAnsi(text: string | ParsedText): string;
ansiPosition
function
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
function
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;
Extensions package
You can extend the behavior of this lib with extensions:
Finding an ANSI code
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:
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)
Optimizing the parser result
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:
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)
Sequence cutting helper
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>;
Decorate package
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
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.
7 months ago