├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .gitattributes ├── .babelrc ├── .prettierrc ├── .eslintrc ├── commitlint.config.js ├── CHANGELOG.md ├── .npmignore ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── package.json ├── ansicolor.d.ts ├── README.md ├── test.js └── ansicolor.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2016" ] 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "extends": ["plugin:prettier/recommended"] 7 | } 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [1, 'always', ['sentence-case']], 5 | 'header-max-length': [1, 'always', 72] 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Recent Updates / Changelog 2 | 3 | - Each input line now gets wrapped separately, i.e. `'foo\nbar'.red === 'foo'.red + '\n' + 'bar'.red` 4 | - You can now do `ansi.green.inverse.underline.italic ('hello')` (chainable API) 5 | - You can now change the default RGB values for CSS output 6 | - `.parse ()` now returns full span style data (ex. `{ italic: true, bgColor: { name: 'red', dim: true }, ...`) 7 | - `.strip ()` for removing ANSI codes 8 | - `.names` for run-time reflection 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # ESDoc 42 | doc 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # ESDoc 42 | doc 43 | 44 | # VS Code 45 | settings.json 46 | 47 | # Ignore generated file 48 | build/ansicolor.js 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - name: checkout 9 | uses: actions/checkout@v3 10 | 11 | - name: Setup NodeJS 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 20 15 | registry-url: https://registry.npmjs.org 16 | 17 | - name: Install Dependencies 18 | run: npm i 19 | 20 | - name: Format 21 | run: npm run format 22 | 23 | - name: Lint 24 | run: npm run lint 25 | 26 | - name: Test 27 | run: npm run test 28 | 29 | deploy: 30 | runs-on: ubuntu-22.04 31 | needs: test 32 | 33 | # Run only on pushing to master 34 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} 35 | 36 | steps: 37 | - name: checkout 38 | uses: actions/checkout@v3 39 | 40 | - name: Setup NodeJS 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: 20 44 | registry-url: https://registry.npmjs.org 45 | 46 | - name: Install Dependencies 47 | run: npm i 48 | 49 | - name: Build 50 | run: npm run build 51 | 52 | - name: Release to NPM 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | # Enable npm provenance - (doing here so when we have tests that actually publish to local npm registry they won't fail) 57 | npm_config_provenance: true 58 | run: npx semantic-release 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ansicolor", 3 | "version": "2.0.2", 4 | "description": "A JavaScript ANSI color/style management. ANSI parsing. ANSI to CSS. Small, clean, no dependencies.", 5 | "main": "./build/ansicolor.js", 6 | "types": "./ansicolor.d.ts", 7 | "scripts": { 8 | "babel": "babel ansicolor.js --source-maps inline --out-file ./build/ansicolor.js", 9 | "build": "npm run babel", 10 | "test": "npm run build && nyc --reporter=html --reporter=text env ANSICOLOR_MODULE='./build/ansicolor' mocha --reporter spec", 11 | "autotest": "env ANSICOLOR_MODULE='./ansicolor' mocha --reporter spec --watch", 12 | "prepare": "husky install", 13 | "format": "prettier --write ansicolor.js test.js", 14 | "lint": "eslint ansicolor.js test.js --fix", 15 | "semantic-release": "semantic-release" 16 | }, 17 | "lint-staged": { 18 | "*.js": ["prettier --write", "eslint --fix"] 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/xpl/ansicolor.git" 23 | }, 24 | "exports": { 25 | "import": "./build/ansicolor.mjs", 26 | "require": "./build/ansicolor.js", 27 | "types": "./ansicolor.d.ts" 28 | }, 29 | "keywords": [ 30 | "ANSI", 31 | "ansi to css", 32 | "code", 33 | "codes", 34 | "color", 35 | "colors", 36 | "text", 37 | "command-line", 38 | "command line", 39 | "sequence", 40 | "control", 41 | "formatting", 42 | "cli", 43 | "shell", 44 | "escape", 45 | "escapes", 46 | "red", 47 | "green", 48 | "blue", 49 | "cyan", 50 | "magenta", 51 | "yellow", 52 | "dim", 53 | "bright", 54 | "background", 55 | "color logging", 56 | "colored logging", 57 | "colored log", 58 | "log with colors", 59 | "log colors", 60 | "color helper", 61 | "colorize", 62 | "color output", 63 | "ansi color", 64 | "ansi-color", 65 | "ansicolor", 66 | "ansi coloring", 67 | "colored strings", 68 | "terminal colors", 69 | "ansi styles", 70 | "strip ansi codes", 71 | "parse ansi", 72 | "ansi parser", 73 | "ansi to html", 74 | "ansi for web", 75 | "web ansi", 76 | "css ansi", 77 | "terminal colors emulation", 78 | "console", 79 | "console colors", 80 | "ansi console", 81 | "logging", 82 | "log", 83 | "chrome", 84 | "chrome devtools", 85 | "web inspector", 86 | "console.log", 87 | "developer tools", 88 | "devtools", 89 | "tty colors", 90 | "tty", 91 | "rainbow" 92 | ], 93 | "author": "Vitaly Gordon ", 94 | "license": "Unlicense", 95 | "bugs": { 96 | "url": "https://github.com/xpl/ansicolor/issues" 97 | }, 98 | "homepage": "https://xpl.github.io/ansicolor", 99 | "devDependencies": { 100 | "@commitlint/cli": "^17.3.0", 101 | "@commitlint/config-conventional": "^17.3.0", 102 | "@types/node": "^13.7.0", 103 | "babel-cli": "^6.26.0", 104 | "babel-preset-es2016": "^6.24.1", 105 | "eslint": "^8.31.0", 106 | "eslint-config-prettier": "^8.6.0", 107 | "eslint-plugin-prettier": "^4.2.1", 108 | "husky": "^8.0.3", 109 | "istanbul": "^0.4.5", 110 | "lint-staged": "^15.0.2", 111 | "mocha": "^5.2.0", 112 | "nyc": "^14.1.1", 113 | "prettier": "^2.8.1", 114 | "semantic-release": "^19.0.5" 115 | }, 116 | "publishConfig": { 117 | "access": "public" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ansicolor.d.ts: -------------------------------------------------------------------------------- 1 | export declare interface ParsedColor { 2 | name?: string; 3 | bright?: boolean; 4 | dim?: boolean; 5 | } 6 | 7 | export declare interface ParsedSpan { 8 | 9 | text: string; 10 | css: string; 11 | italic?: boolean; 12 | bold?: boolean; 13 | color?: ParsedColor; 14 | bgColor?: ParsedColor; 15 | } 16 | 17 | export declare interface AnsiColored extends Iterator { 18 | 19 | readonly spans: ParsedSpan[]; 20 | readonly str: string; 21 | readonly asChromeConsoleLogArguments: string[]; 22 | } 23 | 24 | export declare interface RGBValues { 25 | 26 | black: [number, number, number]; 27 | darkGray: [number, number, number]; 28 | lightGray: [number, number, number]; 29 | white: [number, number, number]; 30 | 31 | red: [number, number, number]; 32 | lightRed: [number, number, number]; 33 | 34 | green: [number, number, number]; 35 | lightGreen: [number, number, number]; 36 | 37 | yellow: [number, number, number]; 38 | lightYellow: [number, number, number]; 39 | 40 | blue: [number, number, number]; 41 | lightBlue: [number, number, number]; 42 | 43 | magenta: [number, number, number]; 44 | lightMagenta: [number, number, number]; 45 | 46 | cyan: [number, number, number]; 47 | lightCyan: [number, number, number]; 48 | } 49 | 50 | export declare interface AnsicolorMethods { 51 | 52 | (text: string|number|null|undefined): string; // applies the style to the string 53 | 54 | default: AnsicolorMethods; 55 | white: AnsicolorMethods; 56 | black: AnsicolorMethods; 57 | red: AnsicolorMethods; 58 | green: AnsicolorMethods; 59 | yellow: AnsicolorMethods; 60 | blue: AnsicolorMethods; 61 | magenta: AnsicolorMethods; 62 | cyan: AnsicolorMethods; 63 | 64 | darkGray: AnsicolorMethods; 65 | lightGray: AnsicolorMethods; 66 | lightRed: AnsicolorMethods; 67 | lightGreen: AnsicolorMethods; 68 | lightYellow: AnsicolorMethods; 69 | lightBlue: AnsicolorMethods; 70 | lightMagenta: AnsicolorMethods; 71 | lightCyan: AnsicolorMethods; 72 | 73 | bright: AnsicolorMethods; 74 | dim: AnsicolorMethods; 75 | italic: AnsicolorMethods; 76 | underline: AnsicolorMethods; 77 | inverse: AnsicolorMethods; 78 | 79 | bgDefault: AnsicolorMethods; 80 | bgWhite: AnsicolorMethods; 81 | bgBlack: AnsicolorMethods; 82 | bgRed: AnsicolorMethods; 83 | bgGreen: AnsicolorMethods; 84 | bgYellow: AnsicolorMethods; 85 | bgBlue: AnsicolorMethods; 86 | bgMagenta: AnsicolorMethods; 87 | bgCyan: AnsicolorMethods; 88 | 89 | bgDarkGray: AnsicolorMethods; 90 | bgLightGray: AnsicolorMethods; 91 | bgLightRed: AnsicolorMethods; 92 | bgLightGreen: AnsicolorMethods; 93 | bgLightYellow: AnsicolorMethods; 94 | bgLightBlue: AnsicolorMethods; 95 | bgLightMagenta: AnsicolorMethods; 96 | bgLightCyan: AnsicolorMethods; 97 | } 98 | 99 | export declare class ansicolor { 100 | 101 | static get nice (): ansicolor; // installs unsafe String extensions when accessed 102 | 103 | static get rgb (): RGBValues; 104 | static set rgb (newSchema: RGBValues); 105 | 106 | static parse (text: string): AnsiColored; 107 | static strip (text: string): string; 108 | static isEscaped (x?: any): boolean; 109 | 110 | static get default (): AnsicolorMethods; 111 | static get white (): AnsicolorMethods; 112 | static get black (): AnsicolorMethods; 113 | static get red (): AnsicolorMethods; 114 | static get green (): AnsicolorMethods; 115 | static get yellow (): AnsicolorMethods; 116 | static get blue (): AnsicolorMethods; 117 | static get magenta (): AnsicolorMethods; 118 | static get cyan (): AnsicolorMethods; 119 | 120 | static get darkGray (): AnsicolorMethods; 121 | static get lightGray (): AnsicolorMethods; 122 | static get lightRed (): AnsicolorMethods; 123 | static get lightGreen (): AnsicolorMethods; 124 | static get lightYellow (): AnsicolorMethods; 125 | static get lightBlue (): AnsicolorMethods; 126 | static get lightMagenta (): AnsicolorMethods; 127 | static get lightCyan (): AnsicolorMethods; 128 | 129 | static get bright (): AnsicolorMethods; 130 | static get dim (): AnsicolorMethods; 131 | static get italic (): AnsicolorMethods; 132 | static get underline (): AnsicolorMethods; 133 | static get inverse (): AnsicolorMethods; 134 | 135 | static get bgDefault (): AnsicolorMethods; 136 | static get bgWhite (): AnsicolorMethods; 137 | static get bgBlack (): AnsicolorMethods; 138 | static get bgRed (): AnsicolorMethods; 139 | static get bgGreen (): AnsicolorMethods; 140 | static get bgYellow (): AnsicolorMethods; 141 | static get bgBlue (): AnsicolorMethods; 142 | static get bgMagenta (): AnsicolorMethods; 143 | static get bgCyan (): AnsicolorMethods; 144 | 145 | static get bgDarkGray (): AnsicolorMethods; 146 | static get bgLightGray (): AnsicolorMethods; 147 | static get bgLightRed (): AnsicolorMethods; 148 | static get bgLightGreen (): AnsicolorMethods; 149 | static get bgLightYellow (): AnsicolorMethods; 150 | static get bgLightBlue (): AnsicolorMethods; 151 | static get bgLightMagenta (): AnsicolorMethods; 152 | static get bgLightCyan (): AnsicolorMethods; 153 | } 154 | 155 | export function parse (text: string): AnsiColored; 156 | export function parseIterator (textOrGetText: string | (() => string)): Iterator; 157 | export function strip (text: string): string; 158 | export function isEscaped (x?: any): boolean; 159 | 160 | export const white: AnsicolorMethods; 161 | export const black: AnsicolorMethods; 162 | export const red: AnsicolorMethods; 163 | export const green: AnsicolorMethods; 164 | export const yellow: AnsicolorMethods; 165 | export const blue: AnsicolorMethods; 166 | export const magenta: AnsicolorMethods; 167 | export const cyan: AnsicolorMethods; 168 | 169 | export const darkGray: AnsicolorMethods; 170 | export const lightGray: AnsicolorMethods; 171 | export const lightRed: AnsicolorMethods; 172 | export const lightGreen: AnsicolorMethods; 173 | export const lightYellow: AnsicolorMethods; 174 | export const lightBlue: AnsicolorMethods; 175 | export const lightMagenta: AnsicolorMethods; 176 | export const lightCyan: AnsicolorMethods; 177 | 178 | export const bright: AnsicolorMethods; 179 | export const dim: AnsicolorMethods; 180 | export const italic: AnsicolorMethods; 181 | export const underline: AnsicolorMethods; 182 | export const inverse: AnsicolorMethods; 183 | 184 | export const bgDefault: AnsicolorMethods; 185 | export const bgWhite: AnsicolorMethods; 186 | export const bgBlack: AnsicolorMethods; 187 | export const bgRed: AnsicolorMethods; 188 | export const bgGreen: AnsicolorMethods; 189 | export const bgYellow: AnsicolorMethods; 190 | export const bgBlue: AnsicolorMethods; 191 | export const bgMagenta: AnsicolorMethods; 192 | export const bgCyan: AnsicolorMethods; 193 | 194 | export const bgDarkGray: AnsicolorMethods; 195 | export const bgLightGray: AnsicolorMethods; 196 | export const bgLightRed: AnsicolorMethods; 197 | export const bgLightGreen: AnsicolorMethods; 198 | export const bgLightYellow: AnsicolorMethods; 199 | export const bgLightBlue: AnsicolorMethods; 200 | export const bgLightMagenta: AnsicolorMethods; 201 | export const bgLightCyan: AnsicolorMethods; 202 | 203 | export default ansicolor; 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansicolor 2 | 3 | [![npm](https://img.shields.io/npm/v/ansicolor.svg)](https://npmjs.com/package/ansicolor) 4 | 5 | A JavaScript ANSI color/style management. ANSI parsing. ANSI to CSS. Small, clean, no dependencies. 6 | 7 | ```bash 8 | npm install ansicolor 9 | ``` 10 | 11 | ## What For 12 | 13 | - String coloring with ANSI escape codes 14 | - Solves the [style hierarchy problem](#why-another-one) (when other similar tools fail) 15 | - Parsing/removing ANSI style data from strings 16 | - Converting ANSI styles to CSS or a Chrome DevTools-compatible output 17 | - A middleware for your [platform-agnostic logging system](https://github.com/xpl/ololog) 18 | 19 | ## Why Another One? 20 | 21 | Other tools lack consistency, failing to solve a simple hierarchy problem: 22 | 23 | ```javascript 24 | require ('colors') // a popular color utility 25 | 26 | console.log (('foo'.cyan + 'bar').red) 27 | ``` 28 | 29 | ![pic](http://cdn.jpg.wtf/futurico/85/9b/1470626860-859b24350e22df74fd7497e9dc0d8d42.png) 30 | 31 | WTF?! The `bar` word above should be rendered in red, but it's not! That sucks. It's because ANSI codes are linear, not hierarchical (as with XML/HTML). A special kind of magic is needed to make this work. **Ansicolor** does that magic for you: 32 | 33 | ```javascript 34 | require ('ansicolor').nice // .nice for unsafe String extensions 35 | 36 | console.log (('foo'.cyan + 'bar').red) 37 | ``` 38 | 39 | ![pic](http://cdn.jpg.wtf/futurico/3c/61/1470626989-3c61b64d0690b0b413be367841650426.png) 40 | 41 | Nice! 42 | 43 | ## Crash Course 44 | 45 | Importing (as methods): 46 | 47 | ```javascript 48 | import { green, inverse, bgLightCyan, underline, dim } from 'ansicolor' 49 | ``` 50 | ```javascript 51 | const { green, inverse, bgLightCyan, underline, dim } = require ('ansicolor') 52 | ``` 53 | 54 | Usage: 55 | 56 | ```javascript 57 | console.log ('foo' + green (inverse (bgLightCyan ('bar')) + 'baz') + 'qux') 58 | ``` 59 | ```javascript 60 | console.log (underline.bright.green ('foo' + dim.red.bgLightCyan ('bar'))) // method chaining 61 | ``` 62 | 63 | Importing (as object): 64 | 65 | ```javascript 66 | import { ansicolor, ParsedSpan } from 'ansicolor' // along with type definitions 67 | ``` 68 | ```javascript 69 | import ansicolor from 'ansicolor' 70 | ``` 71 | 72 | ### Nice Mode (not recommended) 73 | 74 | ```javascript 75 | const ansi = require ('ansicolor').nice 76 | ``` 77 | 78 | The `('ansicolor').nice` export defines styling APIs on the `String` prototype directly. It uses an ad-hoc DSL (sort of) for infix-style string coloring. The `nice` is convenient, but not safe, avoid using it in public modules, as it alters global objects, and that might cause potential hard-to-debug compatibility issues. 79 | 80 | ```javascript 81 | console.log ('foo'.red.bright + 'bar'.bgYellow.underline.dim) 82 | ``` 83 | 84 | ### Supported Styles 85 | 86 | ```javascript 87 | 'foreground colors' 88 | .red.green.yellow.blue.magenta.cyan.white.darkGray.black 89 | ``` 90 | ```javascript 91 | 'light foreground colors' 92 | .lightRed.lightGreen.lightYellow.lightBlue.lightMagenta.lightCyan.lightGray 93 | ``` 94 | ```javascript 95 | 'background colors' 96 | .bgRed.bgGreen.bgYellow.bgBlue.bgMagenta.bgCyan.bgWhite.bgDarkGray.bgBlack 97 | ``` 98 | ```javascript 99 | 'light background colors' 100 | .bgLightRed.bgLightGreen.bgLightYellow.bgLightBlue.bgLightMagenta.bgLightCyan.bgLightGray 101 | ``` 102 | ```javascript 103 | 'styles' 104 | .bright.dim.italic.underline.inverse // your platform should support italic 105 | ``` 106 | 107 | You also can obtain all those style names (for reflection purposes): 108 | 109 | ```javascript 110 | const { names } = require ('ansicolor') 111 | 112 | names // ['red', 'green', ... 113 | ``` 114 | 115 | ## Removing ANSI Styles From Strings 116 | 117 | ```javascript 118 | const { strip } = require ('ansicolor') 119 | 120 | strip ('\u001b[0m\u001b[4m\u001b[42m\u001b[31mfoo\u001b[39m\u001b[49m\u001b[24mfoo\u001b[0m')) // 'foofoo' 121 | ``` 122 | 123 | ## Checking If Strings Contain ANSI Codes 124 | 125 | ```javascript 126 | const { isEscaped, green } = require ('ansicolor') 127 | 128 | isEscaped ('text') // false 129 | isEscaped (green ('text')) // true 130 | ``` 131 | 132 | 133 | ## Converting to CSS/HTML 134 | 135 | Inspection of ANSI styles in arbitrary strings is essential when implementing platform-agnostic logging — that piece of code is available under command line interface and in a browser as well. Here's an example of how you would parse a colored string into an array-like structure. That structure can be traversed later to build HTML/JSON/XML or any other markup/syntax. 136 | 137 | ```javascript 138 | const { parse } = require ('ansicolor') 139 | 140 | const parsed = parse ('foo'.bgLightRed.bright.italic + 'bar'.red.dim) 141 | ``` 142 | 143 | The `ansi.parse ()` method will return a pseudo-array of styled spans, you can iterate over it with a `for ... of` loop and convert it to an array with the *spread operator* (`...`). Also, there's the `.spans` property for obtaining the already-spread array directly: 144 | 145 | ```javascript 146 | assert.deepEqual (parsed.spans /* or [...parsed] */, 147 | 148 | [ { css: 'font-weight: bold;font-style: italic;background:rgba(255,51,0,1);', 149 | italic: true, 150 | bold: true, 151 | color: { bright: true }, 152 | bgColor: { name: 'lightRed' }, 153 | text: 'foo' }, 154 | 155 | { css: 'color:rgba(204,0,0,0.5);', 156 | color: { name: 'red', dim: true }, 157 | text: 'bar' } ]) 158 | ``` 159 | 160 | ### Custom Color Themes 161 | 162 | You can change default RGB values (won't work in terminals, affects only the ANSI→CSS transformation part): 163 | 164 | ```javascript 165 | const ansi = require ('ansicolor') 166 | 167 | ansi.rgb = { 168 | 169 | black: [0, 0, 0], 170 | darkGray: [100, 100, 100], 171 | lightGray: [200, 200, 200], 172 | white: [255, 255, 255], 173 | 174 | red: [204, 0, 0], 175 | lightRed: [255, 51, 0], 176 | 177 | green: [0, 204, 0], 178 | lightGreen: [51, 204, 51], 179 | 180 | yellow: [204, 102, 0], 181 | lightYellow: [255, 153, 51], 182 | 183 | blue: [0, 0, 255], 184 | lightBlue: [26, 140, 255], 185 | 186 | magenta: [204, 0, 204], 187 | lightMagenta: [255, 0, 255], 188 | 189 | cyan: [0, 153, 255], 190 | lightCyan: [0, 204, 255], 191 | } 192 | ``` 193 | 194 | ## Chrome DevTools Compatibility 195 | 196 | Web browsers usually implement their own proprietary CSS-based color formats for `console.log` and most of them fail to display standard ANSI colors. _Ansicolor_ offers you a helper method to convert ANSI-styled strings to browser-compatible argument lists acceptable by Chrome's `console.log`: 197 | 198 | ```javascript 199 | const { bgGreen, red, parse } = require ('ansicolor') 200 | 201 | const string = 'foo' + bgGreen (red.underline.bright.inverse ('bar') + 'baz') 202 | const parsed = parse (string) 203 | 204 | console.log (...parsed.asChromeConsoleLogArguments) // prints with colors in Chrome! 205 | ``` 206 | 207 | Here's what the format looks like: 208 | 209 | ```javascript 210 | parsed.asChromeConsoleLogArguments // [ "%cfoo%cbar%cbaz", 211 | // "", 212 | // "font-weight: bold;text-decoration: underline;background:rgba(255,51,0,1);color:rgba(0,204,0,1);", 213 | // "background:rgba(0,204,0,1);" 214 | // ] 215 | ``` 216 | 217 | Play with this feature online: [demo page](https://xpl.github.io/ololog/). Open the DevTools console and type expressions in the input box to see colored console output. 218 | 219 | Happy logging! 220 | 221 | ## Projects That Use `ansicolor` 222 | 223 | - [**Ololog!**](https://github.com/xpl/ololog) — a better `console.log` for the log-driven debugging junkies 224 | - [**CCXT**](https://github.com/ccxt/ccxt) — a cryptocurrency trading API with 130+ exchanges 225 | - [**Grafana**](https://github.com/grafana/grafana) — beautiful monitoring & metric analytics & dashboards 226 | 227 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* ------------------------------------------------------------------------ */ 4 | 5 | const assert = require ('assert'), 6 | ansi = require (process.env.ANSICOLOR_MODULE).nice 7 | 8 | /* ------------------------------------------------------------------------ */ 9 | 10 | describe ('ansicolor', () => { 11 | 12 | const dbg = s => s.replace (/\u001b\[(\d+)m/g, '\u001b[36m{$1}\u001b[39m') 13 | 14 | const same = (a, b) => { 15 | 16 | console.log ('\n') 17 | 18 | console.log ('expected:', b) 19 | console.log (' ', dbg (b), '\n') 20 | 21 | console.log ('actual :', a) 22 | console.log (' ', dbg (a), '\n') 23 | 24 | assert.equal (a, b) 25 | } 26 | 27 | it ('works', () => { 28 | 29 | same ('foo' + ansi.green (ansi.inverse (ansi.bgLightCyan ('bar') + 'baz') + 'qux'), 30 | 'foo\u001b[32m\u001b[7m\u001b[106mbar\u001b[49mbaz\u001b[27mqux\u001b[39m') 31 | }) 32 | 33 | it ('chaining works', () => { 34 | 35 | same (ansi.underline.bright.green ('chai' + ansi.dim.red.bgLightCyan ('ning')), 36 | '\u001b[4m\u001b[22m\u001b[1m\u001b[32mchai\u001b[22m\u001b[2m\u001b[31m\u001b[106mning\u001b[49m\u001b[32m\u001b[22m\u001b[1m\u001b[39m\u001b[22m\u001b[24m') 37 | }) 38 | 39 | it ('nice mode works', () => { 40 | 41 | ansi.nice // shouldn't mess up with repeated calls 42 | ansi.nice 43 | 44 | same ('foo' + ('bar'.red.underline.bright + 'baz').green.underline + 'qux', 45 | 'foo\u001b[4m\u001b[32m\u001b[22m\u001b[1m\u001b[4m\u001b[31mbar\u001b[32m\u001b[4m\u001b[22mbaz\u001b[39m\u001b[24mqux') 46 | }) 47 | 48 | it ('brightness hierarchy works', () => { 49 | 50 | same (('foo' + 'bar'.dim + 'baz').bright, '\u001b[22m\u001b[1mfoo\u001b[22m\u001b[2mbar\u001b[22m\u001b[1mbaz\u001b[22m') 51 | }) 52 | 53 | it ('hierarchy works', () => { 54 | 55 | same ((('red'.red + 'green').green + 'blue').blue, '\u001b[34m\u001b[32m\u001b[31mred\u001b[32mgreen\u001b[34mblue\u001b[39m') 56 | 57 | same (('foo'.cyan + 'bar').red, '\u001b[31m\u001b[36mfoo\u001b[31mbar\u001b[39m') 58 | same (('foo'.bgCyan + 'bar').bgRed, '\u001b[41m\u001b[46mfoo\u001b[41mbar\u001b[49m') 59 | same (('foo'.bgLightCyan + 'bar').bgLightRed, '\u001b[101m\u001b[106mfoo\u001b[101mbar\u001b[49m') 60 | same (('foo'.underline + 'bar').underline, '\u001b[4m\u001b[4mfoo\u001b[4mbar\u001b[24m') 61 | 62 | same (('foo'.bright + 'bar').bright, '\u001b[22m\u001b[1m\u001b[22m\u001b[1mfoo\u001b[22m\u001b[1mbar\u001b[22m') 63 | same (('foo'.dim + 'bar').dim, '\u001b[22m\u001b[2m\u001b[22m\u001b[2mfoo\u001b[22m\u001b[2mbar\u001b[22m') 64 | same (('foo'.inverse + 'bar').inverse, '\u001b[7m\u001b[7mfoo\u001b[7mbar\u001b[27m') 65 | }) 66 | 67 | it ('basic parsing works', () => { 68 | 69 | const parsed = ansi.parse ('foo'.bgLightRed.bright.italic.underline + 'bar'.red.dim) 70 | 71 | // assert.deepEqual ([...parsed], parsed.spans) // not working in node v4 72 | assert.equal (parsed[Symbol.iterator] ().next ().value.text, 'foo') 73 | 74 | assert.deepEqual (parsed.spans, [ 75 | { 76 | css: 'font-weight: bold;font-style: italic;text-decoration: underline;background:rgba(255,51,0,1);', 77 | italic: true, 78 | bold: true, 79 | underline: true, 80 | bright: false, 81 | dim: false, 82 | inverse: false, 83 | color: { bright: true, 84 | dim: false, 85 | name: undefined, 86 | }, 87 | bgColor: { 88 | bright: false, 89 | dim: false, 90 | name: 'lightRed' 91 | }, 92 | text: 'foo', 93 | code: { 94 | isBrightness: false, 95 | str: '\u001b[49m', 96 | subtype: 'default', 97 | type: 'bgColor', 98 | value: 49 99 | } 100 | }, 101 | { 102 | bgColor: undefined, 103 | bold: false, 104 | bright: false, 105 | code: { 106 | isBrightness: false, 107 | str: "\u001b[39m", 108 | subtype: "default", 109 | type: "color", 110 | value: 39 111 | }, 112 | color: { 113 | bright: false, 114 | dim: true, 115 | name: "red", 116 | }, 117 | css: "color:rgba(204,0,0,0.5);", 118 | dim: false, 119 | inverse: false, 120 | italic: false, 121 | text: "bar", 122 | underline: false, 123 | } 124 | ]) 125 | }) 126 | 127 | it ('brightness semantics (CSS)', () => { 128 | 129 | const parsed = ansi.parse ('foo'.bright.red) 130 | 131 | assert.deepEqual (parsed.spans,[{ 132 | bgColor: undefined, 133 | bold: true, 134 | bright: false, 135 | code: { 136 | isBrightness: true, 137 | str: '\u001b[22m', 138 | subtype: 'dim', 139 | type: 'unstyle', 140 | value: 22, 141 | }, 142 | color: { 143 | bright: true, 144 | dim: false, 145 | name: 'red', 146 | }, 147 | css: 'font-weight: bold;color:rgba(255,51,0,1);', 148 | dim: false, 149 | inverse: false, 150 | italic: false, 151 | text: 'foo', 152 | underline: false, 153 | }]) 154 | }) 155 | 156 | it ('asChromeConsoleLogArguments works', () => { 157 | 158 | const parsed = ansi.parse ('foo' + ('bar'.red.underline.bright.inverse + 'baz').bgGreen) 159 | 160 | assert.deepEqual (parsed.asChromeConsoleLogArguments, parsed.browserConsoleArguments) // legacy API 161 | 162 | assert.deepEqual (parsed.asChromeConsoleLogArguments, [ 163 | 164 | "%cfoo%cbar%cbaz", 165 | "", 166 | "font-weight: bold;text-decoration: underline;background:rgba(255,51,0,1);color:rgba(0,204,0,1);", 167 | "background:rgba(0,204,0,1);" 168 | ]) 169 | }) 170 | 171 | it ('.dim works in CSS (there was a bug)', () => { 172 | 173 | assert.deepEqual (ansi.parse ('foo'.dim).spans,[ 174 | { 175 | bgColor: undefined, 176 | bold: false, 177 | bright: false, 178 | code: { 179 | isBrightness: true, 180 | str: '\u001b[22m', 181 | subtype: 'dim', 182 | type: 'unstyle', 183 | value: 22, 184 | }, 185 | color: { 186 | bright: false, 187 | dim: true, 188 | name: undefined, 189 | }, 190 | css: 'color:rgba(0,0,0,0.5);', 191 | dim: false, 192 | inverse: false, 193 | italic: false, 194 | text: 'foo', 195 | underline: false, 196 | } 197 | ]) 198 | }) 199 | 200 | it ('stripping works', () => { // clauses were copypasted from strip-ansi 201 | 202 | assert.equal ('foofoo', ansi.strip ('\u001b[0m\u001b[4m\u001b[42m\u001b[31mfoo\u001b[39m\u001b[49m\u001b[24mfoo\u001b[0m')) 203 | assert.equal ('bar', ansi.strip ('\x1b[0;33;49;3;9;4mbar\x1b[0m')) 204 | }) 205 | 206 | it ('color names enumeration works', () => { 207 | 208 | assert.deepEqual (ansi.names, [ 209 | 'black', 210 | 'bgBlack', 211 | 'red', 212 | 'bgRed', 213 | 'green', 214 | 'bgGreen', 215 | 'yellow', 216 | 'bgYellow', 217 | 'blue', 218 | 'bgBlue', 219 | 'magenta', 220 | 'bgMagenta', 221 | 'cyan', 222 | 'bgCyan', 223 | 'lightGray', 224 | 'bgLightGray', 225 | 'default', 226 | 'bgDefault', 227 | 'darkGray', 228 | 'bgDarkGray', 229 | 'lightRed', 230 | 'bgLightRed', 231 | 'lightGreen', 232 | 'bgLightGreen', 233 | 'lightYellow', 234 | 'bgLightYellow', 235 | 'lightBlue', 236 | 'bgLightBlue', 237 | 'lightMagenta', 238 | 'bgLightMagenta', 239 | 'lightCyan', 240 | 'bgLightCyan', 241 | 'white', 242 | 'bgWhite', 243 | 'bgBrightRed', 244 | 'bgBrightGreen', 245 | 'bgBrightYellow', 246 | 'bgBrightBlue', 247 | 'bgBrightMagenta', 248 | 'bgBrightCyan', 249 | 'bright', 250 | 'dim', 251 | 'italic', 252 | 'underline', 253 | 'inverse' 254 | ]) 255 | }) 256 | 257 | it ('type coercion works', () => { 258 | 259 | assert.equal (ansi.red (123), ansi.red ('123')) 260 | }) 261 | 262 | it ('newline separation works', () => { 263 | 264 | assert.equal ('foo\nbar\nbaz'.red, 'foo'.red + '\n' + 'bar'.red + '\n' + 'baz'.red) 265 | }) 266 | 267 | it ('inverse works', () => { 268 | 269 | same ('bgRed.inverse'.bgRed.inverse, '\u001b[7m\u001b[41mbgRed.inverse\u001b[49m\u001b[27m') 270 | 271 | assert.equal (ansi.parse ('foo'.bgRed.inverse).spans[0].css, 'background:rgba(255,255,255,1);color:rgba(204,0,0,1);') 272 | }) 273 | 274 | it ('.str works', () => { 275 | 276 | assert.equal (new ansi ('foo\u001b[32m\u001b[7m\u001b[106mbar\u001b[49mbaz\u001b[27mqux\u001b[39m').str, 277 | 'foo\u001b[32m\u001b[7m\u001b[106mbar\u001b[49mbaz\u001b[27mqux\u001b[39m') 278 | }) 279 | 280 | it ('compatible with previous versions where Light was called Bright in bg methods', () => { 281 | 282 | same ('foo'.bgLightCyan, 'foo'.bgBrightCyan) 283 | }) 284 | 285 | it ('explicit reset code works', () => { 286 | 287 | assert.deepEqual (ansi.parse ('\u001b[1m\u001b[31mbold_red\u001b[0mreset').spans, [ 288 | { 289 | bgColor: undefined, 290 | bold: true, 291 | bright: false, 292 | code: { 293 | isBrightness: false, 294 | str: '\u001b[0m', 295 | subtype: '', 296 | type: 'style', 297 | value: 0, 298 | }, 299 | color: { 300 | bright: true, 301 | dim: false, 302 | name: 'red', 303 | }, 304 | css: 'font-weight: bold;color:rgba(255,51,0,1);', 305 | dim: false, 306 | inverse: false, 307 | italic: false, 308 | text: 'bold_red', 309 | underline: false, 310 | }, 311 | { 312 | bgColor: undefined, 313 | bold: false, 314 | bright: false, 315 | code: { 316 | isBrightness: false, 317 | str: '', 318 | subtype: undefined, 319 | type: undefined, 320 | value: undefined, 321 | }, 322 | color: undefined, 323 | css: '', 324 | dim: false, 325 | inverse: false, 326 | italic: false, 327 | text: 'reset', 328 | underline: false, 329 | } 330 | ]) 331 | }) 332 | 333 | it ('implicit reset code works', () => { 334 | 335 | assert.deepEqual (ansi.parse ('\u001b[1m\u001b[31mbold_red\u001b[mreset').spans, 336 | 337 | [ 338 | { 339 | bgColor: undefined, 340 | bold: true, 341 | bright: false, 342 | code: { 343 | isBrightness: false, 344 | str: '\u001b[0m', 345 | subtype: '', 346 | type: 'style', 347 | value: 0, 348 | }, 349 | color: { 350 | bright: true, 351 | dim: false, 352 | name: 'red', 353 | }, 354 | css: 'font-weight: bold;color:rgba(255,51,0,1);', 355 | dim: false, 356 | inverse: false, 357 | italic: false, 358 | text: 'bold_red', 359 | underline: false, 360 | }, 361 | { 362 | bgColor: undefined, 363 | bold: false, 364 | bright: false, 365 | code: { 366 | isBrightness: false, 367 | str: '', 368 | subtype: undefined, 369 | type: undefined, 370 | value: undefined, 371 | }, 372 | color: undefined, 373 | css: '', 374 | dim: false, 375 | inverse: false, 376 | italic: false, 377 | text: 'reset', 378 | underline: false, 379 | } 380 | ]) 381 | }) 382 | 383 | it ('parsing a string with no codes', () => { 384 | 385 | assert.deepEqual (ansi.parse ('foobar').spans,[ 386 | { 387 | bgColor: undefined, 388 | bold: false, 389 | bright: false, 390 | code: { 391 | isBrightness: false, 392 | str: '', 393 | subtype: undefined, 394 | type: undefined, 395 | value: undefined, 396 | }, 397 | color: undefined, 398 | css: '', 399 | dim: false, 400 | inverse: false, 401 | italic: false, 402 | text: 'foobar', 403 | underline: false, 404 | } 405 | ]) 406 | }) 407 | 408 | it ('combined codes work', () => { 409 | 410 | assert.deepEqual (ansi.parse ('\u001b[2;31mfoo🕵️‍bar\u001b[0mbaz').spans,[ 411 | { 412 | bgColor: undefined, 413 | bold: false, 414 | bright: false, 415 | code: { 416 | isBrightness: false, 417 | str: '\u001b[0m', 418 | subtype: '', 419 | type: 'style', 420 | value: 0, 421 | }, 422 | color: { 423 | bright: false, 424 | dim: true, 425 | name: 'red', 426 | }, 427 | css: 'color:rgba(204,0,0,0.5);', 428 | dim: false, 429 | inverse: false, 430 | italic: false, 431 | text: 'foo🕵️‍bar', 432 | underline: false, 433 | }, 434 | { 435 | bgColor: undefined, 436 | bold: false, 437 | bright: false, 438 | code: { 439 | isBrightness: false, 440 | str: '', 441 | subtype: undefined, 442 | type: undefined, 443 | value: undefined, 444 | }, 445 | color: undefined, 446 | css: '', 447 | dim: false, 448 | inverse: false, 449 | italic: false, 450 | text: 'baz', 451 | underline: false, 452 | } 453 | ]) 454 | }) 455 | 456 | it ('broken codes do not parse', () => { 457 | 458 | assert.equal (ansi.parse ('\u001b2;31mfoo').spans[0].text, '\u001b2;31mfoo') 459 | assert.equal (ansi.parse ('\u001b[2;xmfoo').spans[0].text, '\u001b[2;xmfoo') 460 | assert.equal (ansi.parse ('\u001b[0').spans[0].text, '\u001b[0') 461 | }) 462 | 463 | it ('changing .rgb works', () => { 464 | 465 | ansi.rgb.red = [255,0,0] 466 | 467 | assert.equal (ansi.parse ('foo'.red.bgLightRed).spans[0].css, 'color:rgba(255,0,0,1);background:rgba(255,51,0,1);') 468 | }) 469 | 470 | it ('accepting certain non-string values works', () => { 471 | 472 | same (ansi.cyan(42), '\u001b[36m42\u001b[39m') 473 | same (ansi.cyan(null), '\u001b[36mnull\u001b[39m') 474 | same (ansi.cyan(undefined), '\u001b[36mundefined\u001b[39m') 475 | }) 476 | 477 | it ('isEscaped works', () => { 478 | assert.equal (ansi.isEscaped(ansi.red('foo')), true); 479 | assert.equal (ansi.isEscaped('foo'), false); 480 | assert.equal (ansi.isEscaped(null), false); 481 | assert.equal (ansi.isEscaped(undefined), false); 482 | assert.equal (ansi.isEscaped(42), false); 483 | }) 484 | 485 | it ('accepting non ansii colors works', () => { 486 | assert.doesNotThrow (() => ansi.parse('\u001b[253mtest\u001b[39m')); 487 | }) 488 | }) 489 | 490 | -------------------------------------------------------------------------------- /ansicolor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* ------------------------------------------------------------------------ */ 4 | 5 | const O = Object; 6 | 7 | /* See https://misc.flogisoft.com/bash/tip_colors_and_formatting 8 | ------------------------------------------------------------------------ */ 9 | 10 | const colorCodes = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightGray', '', 'default'], 11 | colorCodesLight = [ 12 | 'darkGray', 13 | 'lightRed', 14 | 'lightGreen', 15 | 'lightYellow', 16 | 'lightBlue', 17 | 'lightMagenta', 18 | 'lightCyan', 19 | 'white', 20 | '' 21 | ], 22 | styleCodes = ['', 'bright', 'dim', 'italic', 'underline', '', '', 'inverse'], 23 | asBright = { 24 | red: 'lightRed', 25 | green: 'lightGreen', 26 | yellow: 'lightYellow', 27 | blue: 'lightBlue', 28 | magenta: 'lightMagenta', 29 | cyan: 'lightCyan', 30 | black: 'darkGray', 31 | lightGray: 'white' 32 | }, 33 | types = { 0: 'style', 2: 'unstyle', 3: 'color', 9: 'colorLight', 4: 'bgColor', 10: 'bgColorLight' }, 34 | subtypes = { 35 | color: colorCodes, 36 | colorLight: colorCodesLight, 37 | bgColor: colorCodes, 38 | bgColorLight: colorCodesLight, 39 | style: styleCodes, 40 | unstyle: styleCodes 41 | }; 42 | 43 | /* ------------------------------------------------------------------------ */ 44 | 45 | class Color { 46 | constructor(background, name, brightness) { 47 | this.background = background; 48 | this.name = name; 49 | this.brightness = brightness; 50 | } 51 | 52 | get inverse() { 53 | return new Color(!this.background, this.name || (this.background ? 'black' : 'white'), this.brightness); 54 | } 55 | 56 | get clean() { 57 | const name = this.name === 'default' ? '' : this.name; 58 | const bright = this.brightness === Code.bright; 59 | const dim = this.brightness === Code.dim; 60 | 61 | if (!name && !bright && !dim) { 62 | return undefined; 63 | } 64 | 65 | return { 66 | name, 67 | bright, 68 | dim 69 | }; 70 | } 71 | 72 | defaultBrightness(value) { 73 | return new Color(this.background, this.name, this.brightness || value); 74 | } 75 | 76 | css(inverted) { 77 | const color = inverted ? this.inverse : this; 78 | 79 | const rgbName = (color.brightness === Code.bright && asBright[color.name]) || color.name; 80 | 81 | const prop = color.background ? 'background:' : 'color:', 82 | rgb = Colors.rgb[rgbName], 83 | alpha = this.brightness === Code.dim ? 0.5 : 1; 84 | 85 | return rgb 86 | ? prop + 'rgba(' + [...rgb, alpha].join(',') + ');' 87 | : !color.background && alpha < 1 88 | ? 'color:rgba(0,0,0,0.5);' 89 | : ''; // Chrome does not support 'opacity' property... 90 | } 91 | } 92 | 93 | /* ------------------------------------------------------------------------ */ 94 | 95 | class Code { 96 | constructor(n) { 97 | let value = undefined; 98 | let type = undefined; 99 | let subtype = undefined; 100 | let str = ''; 101 | let isBrightness = false; 102 | 103 | if (n !== undefined) { 104 | value = Number(n); 105 | type = types[Math.floor(value / 10)]; 106 | if (type === undefined || subtypes[type] === undefined) { 107 | return; 108 | } 109 | subtype = subtypes[type][value % 10]; 110 | str = '\u001b[' + value + 'm'; 111 | isBrightness = value === Code.noBrightness || value === Code.bright || value === Code.dim; 112 | } 113 | 114 | this.value = value; 115 | this.type = type; 116 | this.subtype = subtype; 117 | this.str = str; 118 | this.isBrightness = isBrightness; 119 | } 120 | 121 | static str(x) { 122 | if (x === undefined) return ''; 123 | return '\u001b[' + Number(x) + 'm'; 124 | } 125 | 126 | clone() { 127 | const newCode = new Code(undefined); 128 | newCode.value = this.value; 129 | newCode.type = this.type; 130 | newCode.subtype = this.subtype; 131 | newCode.str = this.str; 132 | newCode.isBrightness = this.isBrightness; 133 | return newCode; 134 | } 135 | } 136 | 137 | /* ------------------------------------------------------------------------ */ 138 | 139 | O.assign(Code, { 140 | reset: 0, 141 | bright: 1, 142 | dim: 2, 143 | inverse: 7, 144 | noBrightness: 22, 145 | noItalic: 23, 146 | noUnderline: 24, 147 | noInverse: 27, 148 | noColor: 39, 149 | noBgColor: 49 150 | }); 151 | 152 | /* ------------------------------------------------------------------------ */ 153 | 154 | const replaceAll = (str, a, b) => str.split(a).join(b); 155 | 156 | /* ANSI brightness codes do not overlap, e.g. "{bright}{dim}foo" will be rendered bright (not dim). 157 | So we fix it by adding brightness canceling before each brightness code, so the former example gets 158 | converted to "{noBrightness}{bright}{noBrightness}{dim}foo" – this way it gets rendered as expected. 159 | */ 160 | 161 | const denormalizeBrightness = (s) => s.replace(/(\u001b\[(1|2)m)/g, '\u001b[22m$1'); 162 | const normalizeBrightness = (s) => s.replace(/\u001b\[22m(\u001b\[(1|2)m)/g, '$1'); 163 | 164 | const wrap = (x, openCode, closeCode) => { 165 | const open = Code.str(openCode), 166 | close = Code.str(closeCode); 167 | 168 | return String(x) 169 | .split('\n') 170 | .map((line) => denormalizeBrightness(open + replaceAll(normalizeBrightness(line), close, open) + close)) 171 | .join('\n'); 172 | }; 173 | 174 | /* ------------------------------------------------------------------------ */ 175 | 176 | const camel = (a, b) => a + b.charAt(0).toUpperCase() + b.slice(1); 177 | 178 | const stringWrappingMethods = (() => 179 | [ 180 | ...colorCodes.map((k, i) => 181 | !k 182 | ? [] 183 | : [ 184 | // color methods 185 | 186 | [k, 30 + i, Code.noColor], 187 | [camel('bg', k), 40 + i, Code.noBgColor] 188 | ] 189 | ), 190 | 191 | ...colorCodesLight.map((k, i) => 192 | !k 193 | ? [] 194 | : [ 195 | // light color methods 196 | 197 | [k, 90 + i, Code.noColor], 198 | [camel('bg', k), 100 + i, Code.noBgColor] 199 | ] 200 | ), 201 | 202 | /* THIS ONE IS FOR BACKWARDS COMPATIBILITY WITH PREVIOUS VERSIONS (had 'bright' instead of 'light' for backgrounds) 203 | */ 204 | ...['', 'BrightRed', 'BrightGreen', 'BrightYellow', 'BrightBlue', 'BrightMagenta', 'BrightCyan'].map((k, i) => 205 | !k ? [] : [['bg' + k, 100 + i, Code.noBgColor]] 206 | ), 207 | 208 | ...styleCodes.map((k, i) => 209 | !k 210 | ? [] 211 | : [ 212 | // style methods 213 | 214 | [k, i, k === 'bright' || k === 'dim' ? Code.noBrightness : 20 + i] 215 | ] 216 | ) 217 | ].reduce((a, b) => a.concat(b)))(); 218 | 219 | /* ------------------------------------------------------------------------ */ 220 | 221 | const assignStringWrappingAPI = (target, wrapBefore = target) => 222 | stringWrappingMethods.reduce( 223 | (memo, [k, open, close]) => 224 | O.defineProperty(memo, k, { 225 | get: () => assignStringWrappingAPI((str) => wrapBefore(wrap(str, open, close))) 226 | }), 227 | 228 | target 229 | ); 230 | 231 | /* ------------------------------------------------------------------------ */ 232 | 233 | const TEXT = 0, 234 | BRACKET = 1, 235 | CODE = 2; 236 | 237 | class Span { 238 | constructor(code, text) { 239 | this.code = code; 240 | this.text = text; 241 | 242 | // Those are added in the actual parse, this is done for performance reasons to have the same hidden class 243 | this.css = ''; 244 | this.color = undefined; 245 | this.bgColor = undefined; 246 | this.bold = undefined; 247 | this.inverse = undefined; 248 | this.italic = undefined; 249 | this.underline = undefined; 250 | this.bright = undefined; 251 | this.dim = undefined; 252 | } 253 | } 254 | 255 | // getString as function instead of string to allow garbage collection 256 | function* rawParse(getString) { 257 | const stateObject = { 258 | state: TEXT, 259 | buffer: '', 260 | text: '', 261 | code: '', 262 | codes: [] 263 | }; 264 | 265 | const ONE_MB = 1048576; 266 | 267 | // Instead of holding the reference to the string we split into chunks of 1MB 268 | // and after processing is finished we can remove the reference so it can be GCed 269 | const chunks = splitStringToChunksOfSize(getString(), ONE_MB); 270 | 271 | for (let i = 0; i < chunks.length; i++) { 272 | const chunk = chunks[i]; 273 | // Free memory for the previous chunk 274 | chunks[i] = undefined; 275 | yield* processChunk(chunk, stateObject); 276 | } 277 | 278 | if (stateObject.state !== TEXT) stateObject.text += stateObject.buffer; 279 | 280 | if (stateObject.text) { 281 | yield new Span(new Code(), stateObject.text); 282 | } 283 | } 284 | 285 | function splitStringToChunksOfSize(str, chunkSize) { 286 | const chunks = []; 287 | const chunksLength = Math.ceil(str.length / chunkSize); 288 | 289 | for (let i = 0, o = 0; i < chunksLength; ++i, o += chunkSize) { 290 | chunks.push(str.substring(o, o + chunkSize)); 291 | } 292 | 293 | return chunks; 294 | } 295 | 296 | function* processChunk(chunk, stateObject) { 297 | const chars = chunk; 298 | const charsLength = chunk.length; 299 | 300 | for (let i = 0; i < charsLength; i++) { 301 | const c = chars[i]; 302 | 303 | stateObject.buffer += c; 304 | 305 | switch (stateObject.state) { 306 | case TEXT: 307 | if (c === '\u001b') { 308 | stateObject.state = BRACKET; 309 | stateObject.buffer = c; 310 | } else { 311 | stateObject.text += c; 312 | } 313 | break; 314 | 315 | case BRACKET: 316 | if (c === '[') { 317 | stateObject.state = CODE; 318 | stateObject.code = ''; 319 | stateObject.codes = []; 320 | } else { 321 | stateObject.state = TEXT; 322 | stateObject.text += stateObject.buffer; 323 | } 324 | break; 325 | 326 | case CODE: 327 | if (c >= '0' && c <= '9') { 328 | stateObject.code += c; 329 | } else if (c === ';') { 330 | stateObject.codes.push(new Code(stateObject.code)); 331 | stateObject.code = ''; 332 | } else if (c === 'm') { 333 | stateObject.code = stateObject.code || '0'; 334 | for (const code of stateObject.codes) { 335 | yield new Span(code, stateObject.text); 336 | stateObject.text = ''; 337 | } 338 | 339 | yield new Span(new Code(stateObject.code), stateObject.text); 340 | stateObject.text = ''; 341 | stateObject.state = TEXT; 342 | } else { 343 | stateObject.state = TEXT; 344 | stateObject.text += stateObject.buffer; 345 | } 346 | } 347 | } 348 | } 349 | 350 | /** 351 | * Parse ansi text 352 | * @param {Generator} rawSpansIterator raw spans iterator 353 | * @return {Generator} 354 | */ 355 | function* parseAnsi(rawSpansIterator) { 356 | let color = new Color(); 357 | let bgColor = new Color(true /* background */); 358 | let brightness = undefined; 359 | let styles = new Set(); 360 | 361 | function reset() { 362 | color = new Color(); 363 | bgColor = new Color(true /* background */); 364 | brightness = undefined; 365 | styles.clear(); 366 | } 367 | 368 | reset(); 369 | 370 | for (const span of rawSpansIterator) { 371 | const c = span.code; 372 | 373 | if (span.text !== '') { 374 | const inverted = styles.has('inverse'); 375 | const underline = styles.has('underline') ? 'text-decoration: underline;' : ''; 376 | const italic = styles.has('italic') ? 'font-style: italic;' : ''; 377 | const bold = brightness === Code.bright ? 'font-weight: bold;' : ''; 378 | 379 | const foreColor = color.defaultBrightness(brightness); 380 | 381 | const newSpan = new Span(span.code ? span.code.clone() : undefined, span.text); 382 | 383 | newSpan.css = span.css ? span.css : bold + italic + underline + foreColor.css(inverted) + bgColor.css(inverted); 384 | newSpan.bold = span.bold ? span.bold : !!bold; 385 | newSpan.color = span.color ? span.color : foreColor.clean; 386 | newSpan.bgColor = span.bgColor ? span.bgColor : bgColor.clean; 387 | newSpan.inverse = inverted; 388 | newSpan.italic = !!italic; 389 | newSpan.underline = !!underline; 390 | newSpan.bright = styles.has('bright'); 391 | newSpan.dim = styles.has('dim'); 392 | 393 | yield newSpan; 394 | } 395 | 396 | if (c.isBrightness) { 397 | brightness = c.value; 398 | continue; 399 | } 400 | 401 | if (span.code.value === undefined) { 402 | continue; 403 | } 404 | 405 | if (span.code.value === Code.reset) { 406 | reset(); 407 | continue; 408 | } 409 | 410 | switch (span.code.type) { 411 | case 'color': 412 | case 'colorLight': 413 | color = new Color(false, c.subtype); 414 | break; 415 | 416 | case 'bgColor': 417 | case 'bgColorLight': 418 | bgColor = new Color(true, c.subtype); 419 | break; 420 | 421 | case 'style': 422 | styles.add(c.subtype); 423 | break; 424 | case 'unstyle': 425 | styles.delete(c.subtype); 426 | break; 427 | } 428 | } 429 | } 430 | 431 | /* ------------------------------------------------------------------------ */ 432 | 433 | /** 434 | * Represents an ANSI-escaped string. 435 | */ 436 | class Colors { 437 | /** 438 | * @param {string} s a string containing ANSI escape codes. 439 | */ 440 | constructor(s) { 441 | this.spans = s ? Array.from(rawParse(typeof s === 'string' ? () => s : s)) : []; 442 | } 443 | 444 | get str() { 445 | return this.spans.reduce((str, p) => str + p.text + p.code.str, ''); 446 | } 447 | 448 | get parsed() { 449 | const newColors = new Colors(); 450 | 451 | newColors.spans = Array.from(parseAnsi(this.spans)); 452 | 453 | return newColors; 454 | } 455 | 456 | /* Outputs with Chrome DevTools-compatible format */ 457 | 458 | get asChromeConsoleLogArguments() { 459 | const spans = this.parsed.spans; 460 | 461 | return [spans.map((s) => '%c' + s.text).join(''), ...spans.map((s) => s.css)]; 462 | } 463 | 464 | get browserConsoleArguments() /* LEGACY, DEPRECATED */ { 465 | return this.asChromeConsoleLogArguments; 466 | } 467 | 468 | /** 469 | * @desc installs String prototype extensions 470 | * @example 471 | * require ('ansicolor').nice 472 | * console.log ('foo'.bright.red) 473 | */ 474 | static get nice() { 475 | Colors.names.forEach((k) => { 476 | if (!(k in String.prototype)) { 477 | O.defineProperty(String.prototype, k, { 478 | get: function () { 479 | return Colors[k](this); 480 | } 481 | }); 482 | } 483 | }); 484 | 485 | return Colors; 486 | } 487 | 488 | /** 489 | * @desc parses a string containing ANSI escape codes 490 | * @return {Colors} parsed representation. 491 | */ 492 | static parse(s) { 493 | return new Colors(s).parsed; 494 | } 495 | 496 | /** 497 | * 498 | * @param {string | () => string} s string or a function returning a string (for large strings you may want to use a function to avoid memory issues) 499 | * @returns {Generator} Spans iterator 500 | */ 501 | static parseIterator(s) { 502 | return parseAnsi(rawParse(typeof s === 'string' ? () => s : s)); 503 | } 504 | 505 | /** 506 | * @desc strips ANSI codes from a string 507 | * @param {string} s a string containing ANSI escape codes. 508 | * @return {string} clean string. 509 | */ 510 | static strip(s) { 511 | return s.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g, ''); // hope V8 caches the regexp 512 | } 513 | 514 | /** 515 | * @desc checks if a value contains ANSI escape codes 516 | * @param {any} s value to check 517 | * @return {boolean} has codes 518 | */ 519 | static isEscaped(s) { 520 | s = String(s); 521 | return Colors.strip(s) !== s; 522 | } 523 | 524 | /** 525 | * @example 526 | * const spans = [...ansi.parse ('\u001b[7m\u001b[7mfoo\u001b[7mbar\u001b[27m')] 527 | */ 528 | [Symbol.iterator]() { 529 | return this.spans[Symbol.iterator](); 530 | } 531 | 532 | /** 533 | * @desc This allows an alternative import style, see https://github.com/xpl/ansicolor/issues/7#issuecomment-578923578 534 | * @example 535 | * import { ansicolor, ParsedSpan } from 'ansicolor' 536 | */ 537 | static get ansicolor() { 538 | return Colors; 539 | } 540 | } 541 | 542 | /* ------------------------------------------------------------------------ */ 543 | 544 | assignStringWrappingAPI(Colors, (str) => str); 545 | 546 | /* ------------------------------------------------------------------------ */ 547 | 548 | Colors.names = stringWrappingMethods.map(([k]) => k); 549 | 550 | /* ------------------------------------------------------------------------ */ 551 | 552 | Colors.rgb = { 553 | black: [0, 0, 0], 554 | darkGray: [100, 100, 100], 555 | lightGray: [200, 200, 200], 556 | white: [255, 255, 255], 557 | 558 | red: [204, 0, 0], 559 | lightRed: [255, 51, 0], 560 | 561 | green: [0, 204, 0], 562 | lightGreen: [51, 204, 51], 563 | 564 | yellow: [204, 102, 0], 565 | lightYellow: [255, 153, 51], 566 | 567 | blue: [0, 0, 255], 568 | lightBlue: [26, 140, 255], 569 | 570 | magenta: [204, 0, 204], 571 | lightMagenta: [255, 0, 255], 572 | 573 | cyan: [0, 153, 255], 574 | lightCyan: [0, 204, 255] 575 | }; 576 | 577 | /* ------------------------------------------------------------------------ */ 578 | 579 | module.exports = Colors; 580 | 581 | /* ------------------------------------------------------------------------ */ 582 | --------------------------------------------------------------------------------