1.0.0 • Published 7 months ago

@macfja/ansi v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

@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:

CodeName
CUUCursor Up
CUDCursor Down
CUFCursor Forward
CUBCursor Back
CNLCursor Next Line
CPLCursor Previous Line
CHACursor Horizontal Absolute
EDErase in Display
ELErase in Line
SUScroll Up
SDScroll Down
CUPCursor Position
HVPHorizontal Vertical Position
AUX On
AUX Off
DSRDevice Status Report
SGRSelect 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 border
  • doubleSquareBox: : Create a box with a continuous double line
  • squareBox: : Create a box with a continuous line
  • curlyBracket: Prefix the text with a big curly bracket
  • padding(): A function to create a padding/margin space around the text
  • boxChar(): 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.
1.0.0

7 months ago