Home Reference Source Test Repository

ansicolor/ansicolor.js

"use strict";

/*  ------------------------------------------------------------------------ */

const O = Object

/*  ------------------------------------------------------------------------ */

const

    colorCodes = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', '', 'default'],
    styleCodes = ['', 'bright', 'dim', 'italic', 'underline', '', '', 'inverse'],

    types = {   0:  'style',
                2:  'unstyle',
                3:  'color',
                4:  'bgColor',
                10: 'bgColorBright' },

    subtypes = {    color:         colorCodes,
                    bgColor:       colorCodes,
                    bgColorBright: colorCodes,
                    style:         styleCodes,
                    unstyle:       styleCodes    }

/*  ------------------------------------------------------------------------ */

const clean = obj => {
                for (const k in obj) { if (!obj[k]) { delete obj[k] } }
                return (O.keys (obj).length === 0) ? undefined : obj
            }

/*  ------------------------------------------------------------------------ */

class Color {

    constructor (background, name, brightness) {

        this.background = background
        this.name = name
        this.brightness = brightness
    }

    get inverse () {
        return new Color (!this.background, this.name || (this.background ? 'black' : 'white'), this.brightness)
    }

    get clean () {
        return clean ({ name:   this.name === 'default' ? '' : this.name,
                        bright: this.brightness === Code.bright,
                        dim:    this.brightness === Code.dim })
    }

    defaultBrightness (value) {

        return new Color (this.background, this.name, this.brightness || value)
    }

    css (inverted) {

        const color = inverted ? this.inverse : this

        const prop = (color.background ? 'background:' : 'color:'),
              rgb  = ((this.brightness === Code.bright) ? Colors.rgbBright : Colors.rgb)[color.name]

        const alpha = (this.brightness === Code.dim) ? 0.5 : 1

        return rgb
                ? (prop + 'rgba(' + [...rgb, alpha].join (',') + ');')
                : ((!color.background && (alpha < 1)) ? 'color:rgba(0,0,0,0.5);' : '') // Chrome does not support 'opacity' property...
    }
}

/*  ------------------------------------------------------------------------ */

class Code {

    constructor (n) {
        if (n !== undefined) { this.value = Number (n) } }

    get type () {
       return types[Math.floor (this.value / 10)] }

    get subtype () {
        return subtypes[this.type][this.value % 10] }

    get str () {
        return (this.value ? ('\u001b\[' + this.value + 'm') : '') }

    static str (x) {
        return new Code (x).str }

    get isBrightness () {
        return (this.value === Code.noBrightness) || (this.value === Code.bright) || (this.value === Code.dim) }
}

/*  ------------------------------------------------------------------------ */

O.assign (Code, {

    bright:       1,
    dim:          2,
    inverse:      7,
    noBrightness: 22,
    noItalic:     23,
    noUnderline:  24,
    noInverse:    27,
    noColor:      39,
    noBgColor:    49
})

/*  ------------------------------------------------------------------------ */

const camel = (a, b) => a + b.charAt (0).toUpperCase () + b.slice (1)

const replaceAll = (str, a, b) => str.split (a).join (b)

/*  ANSI brightness codes do not overlap, e.g. "{bright}{dim}foo" will be rendered bright (not dim).
    So we fix it by adding brightness canceling before each brightness code, so the former example gets
    converted to "{noBrightness}{bright}{noBrightness}{dim}foo" – this way it gets rendered as expected.
 */

const denormalizeBrightness = s => s.replace (/(\u001b\[(1|2)m)/g, '\u001b[22m$1')
const normalizeBrightness = s => s.replace (/\u001b\[22m(\u001b\[(1|2)m)/g, '$1')

const wrap = (x, openCode, closeCode) => {

    const open  = Code.str (openCode),
          close = Code.str (closeCode)

    return String (x)
                .split ('\n')
                .map (line => denormalizeBrightness (open + replaceAll (normalizeBrightness (line), close, open) + close))
                .join ('\n')
}

/*  ------------------------------------------------------------------------ */

const stringWrappingMethods = (() => [

        ...colorCodes.map ((k, i) => !k ? [] : [ // color methods

            [k,                     30 + i,  Code.noColor],
            [camel ('bg', k),       40 + i,  Code.noBgColor],
            [camel ('bgBright', k), 100 + i, Code.noBgColor]
        ]),

        ...styleCodes.map ((k, i) => !k ? [] : [ // style methods

            [k, i, ((k === 'bright') || (k === 'dim')) ? Code.noBrightness : (20 + i)]
        ])
    ]
    .reduce ((a, b) => a.concat (b))
    
) ();

/*  ------------------------------------------------------------------------ */

const assignStringWrappingAPI = (target, wrapBefore = target) =>

    stringWrappingMethods.reduce ((memo, [k, open, close]) =>
                                        O.defineProperty (memo, k, {
                                            get: () => assignStringWrappingAPI (str => wrapBefore (wrap (str, open, close)))
                                        }),

                                  target)

/*  ------------------------------------------------------------------------ */

/**
 * Represents an ANSI-escaped string.
 */
class Colors {

    /**
     * @param {string} s a string containing ANSI escape codes.
     */
    constructor (s) {

        if (s) {

            const r = /\u001b\[(\d+)m/g

            const spans = s.split (/\u001b\[\d+m/)
            const codes = []

            for (let match; match = r.exec (s);) codes.push (match[1])

            this.spans = spans.map ((s, i) => ({ text: s, code: new Code (codes[i]) })) 
        }

        else {
            this.spans = []
        }
    }

    get str () {
        return this.spans.reduce ((str, p) => str + p.text + p.code.str, '')
    }

    get parsed () {

        var color      = new Color (),
            bgColor    = new Color (true /* background */),
            brightness = undefined,
            styles     = new Set ()

        return O.assign (new Colors (), {

            spans: this.spans.map (span => {

                const c = span.code

                const inverted  = styles.has ('inverse'),
                      underline = styles.has ('underline')   ? 'text-decoration: underline;' : '',                      
                      italic    = styles.has ('italic')      ? 'font-style: italic;' : '',
                      bold      = brightness === Code.bright ? 'font-weight: bold;' : ''

                const foreColor = color.defaultBrightness (brightness)

                const styledSpan = O.assign (
                                    { css: bold + italic + underline + foreColor.css (inverted) + bgColor.css (inverted) },
                                        clean ({ bold: !!bold, color: foreColor.clean, bgColor: bgColor.clean }),
                                            span)

                for (const k of styles) { styledSpan[k] = true }

                if (c.isBrightness) {

                    brightness = c.value
                
                } else {

                    switch (span.code.type) {

                        case 'color'        : color   = new Color (false, c.subtype);              break
                        case 'bgColor'      : bgColor = new Color (true,  c.subtype);              break
                        case 'bgColorBright': bgColor = new Color (true,  c.subtype, Code.bright); break

                        case 'style'  : styles.add    (c.subtype); break
                        case 'unstyle': styles.delete (c.subtype); break
                    }
                }

                return styledSpan

            }).filter (s => s.text.length > 0)
        })
    }

/*  Outputs with Chrome DevTools-compatible format     */

    get asChromeConsoleLogArguments () {

        const spans = this.parsed.spans

        return [spans.map (s => ('%c' + s.text)).join (''),
             ...spans.map (s => s.css)]
    }

    get browserConsoleArguments () /* LEGACY, DEPRECATED */ { return this.asChromeConsoleLogArguments }

    /**
     * @desc installs String prototype extensions
     * @example
     * require ('ansicolor').nice
     * console.log ('foo'.bright.red)
     */
    static get nice () {

        Colors.names.forEach (k => {
            if (!(k in String.prototype)) {
                O.defineProperty (String.prototype, k, { get: function () { return Colors[k] (this) } })
            }
        })

        return Colors
    }

    /**
     * @desc parses a string containing ANSI escape codes
     * @return {Colors} parsed representation.
     */
    static parse (s) {
        return new Colors (s).parsed
    }

    /**
     * @desc strips ANSI codes from a string
     * @param {string} s a string containing ANSI escape codes.
     * @return {string} clean string.
     */
    static strip (s) {
        return s.replace (/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g, '') // hope V8 caches the regexp
    }

    /**
     * @example
     * const spans = [...ansi.parse ('\u001b[7m\u001b[7mfoo\u001b[7mbar\u001b[27m')]
     */
    [Symbol.iterator] () {
        return this.spans[Symbol.iterator] ()
    }
}

/*  ------------------------------------------------------------------------ */

assignStringWrappingAPI (Colors, str => str)

/*  ------------------------------------------------------------------------ */

Colors.names = stringWrappingMethods.map (([k]) => k)

/*  ------------------------------------------------------------------------ */

Colors.rgb = {

    black:   [0,     0,   0],
    red:     [204,   0,   0],
    green:   [0,   204,   0],
    yellow:  [204, 102,   0],
    blue:    [0,     0, 255],
    magenta: [204,   0, 204],
    cyan:    [0,   153, 255],
    white:   [255, 255, 255]
}

Colors.rgbBright = {

    black:   [0,     0,   0],
    red:     [255,  51,   0],
    green:   [51,  204,  51],
    yellow:  [255, 153,  51],
    blue:    [26,  140, 255],
    magenta: [255,   0, 255],
    cyan:    [0,   204, 255],
    white:   [255, 255, 255]
}

/*  ------------------------------------------------------------------------ */

module.exports = Colors

/*  ------------------------------------------------------------------------ */