├── CHANGELOG.md ├── .gitattributes ├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── .travis.yml ├── printable-characters.js ├── package.json ├── README.md └── test.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Recent updates 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-es2015-destructuring"] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "script" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | 4 | # Coverage directory used by tools like istanbul 5 | coverage 6 | 7 | # nyc test coverage 8 | .nyc_output -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | 4 | # Coverage directory used by tools like istanbul 5 | coverage 6 | 7 | # nyc test coverage 8 | .nyc_output -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | script: 5 | - set -e 6 | - npm run test 7 | - npm run coveralls 8 | after_success: 9 | - git config --global user.email "travis@travis-ci.org" 10 | - git config --global user.name "Travis CI" 11 | - npm config set git-tag-version=false 12 | - NPM_VERSION=$(npm version patch) 13 | - git commit -a -m "${NPM_VERSION:1}" -m "[ci skip]" 14 | - git remote remove origin 15 | - git remote add origin https://${GITHUB_TOKEN}@github.com/xpl/printable-characters.git 16 | - git push origin HEAD:master 17 | deploy: 18 | provider: npm 19 | email: rocket.mind@gmail.com 20 | api_key: 21 | secure: EOHJmUzqz4QzNyQtRbomfi+dw0WMqw8EvT9hOzaTkw4W35fGyDLUblFFv5Md34h9+0oNNeGvj8/tnnYV0JF+vpH7sPW7jhObT03RvIOfglofIGPzF0OuH0PsWMowhjZwqCqpDDbLDA4sT7pcsjoE7uxIX2K/ekN4E44i8et64mpeHViQbowsbfTqhmsjo7oOmeH28J4dqeWrfyrTS+Ru17mmlyLOr+LTEBiQQYt4oKC+zKeyc5zcF73HAmt5pPDf4ZmXPD6sv6btpYFYYbRp6IBwEkw96MGfu6qD7HB1viIWr/n+le01T8gp2MS857AXERoByTCPWpU9zZ+f9UJOMhpRJjMorbMfolACR/f41ftPqF3dWuVAqzujmgeUEoiG1Gp9VtJqAAEeMSAzbL2dhlRbzPHnSV8ymqhiX6mAeCUXVHFzG/nY+CW2V4XfursD3ZDbgyD/otCKZbkBDmTxG0JdtA7HgMxh9eXRfIqw6LQNL+X1Mhr5nhgrG6u052dm79Is7EBihb5YS48Nl7IqQLijtb2x62YGspVmRI05W4cE7Hr+MHYBTrUSakyva3yKZXcMaLv0x/Wsos6h22qDYJUbv1Fiq1aWog5YvW/bq63PuU0A0AYhffliRkxqhH9s81ECYYVc+1ddtRJLnRGZEQ866mNl/O7C4hztXrbYTyE= 22 | env: 23 | global: 24 | secure: hJDQGV8hM1zxL77khxhtXPbb1lBF9dPmUytcMA3mt6BDyz9+U3fQcpn5CMnwG+Vu03mjSOuY5BfprG2tp+MLmjVnK5ej/Wl/nfFeUAeSshnoWZwTq+vbvURsGBeGKWqnrYK6fZfprcysIEF40gh2RsOfu/pLeZCzgFmRhgyONrBUfIIMH6iordj986S4xWR0bETFlUZ3tSAG9JTADXK1CMIC5fn+UE8vOVC00BR6GDwdsU/cA1J6+HCD1wq6fvEi5gVlzHyiprmYiIgXwK/k6eaGCZhk5jDZ6fL5fW07pGdQRAGb0SqTeyzjy36w1xu7bH3G/ABQPMCg2tzfD2jCKgcja5Dbe7YpZOcx7ArBp8yke0rSuoH0rHK8i4/Ngxsx7xJr4wxfMxpXM82jIi3bZV/ZIDNIE3oLfO/FPvgI+zjckXoKJO7esM09fhaXLqCak6ls3NUy86pXWCn8jiL09kaFl5A5O23BI67zmLtsKejdS+5jU3W/LkW07UM17SiU9CkgEPQi07uW9rJ9dEhhsCRCvH5xxYLf+I/0zMJNGsYOEvo0P7FjI6FAz0E1h7Ktctr0D9GYqpvWggugxoc2G7s1SwrJZmqR3uWMRC8t/4Pd6fyk7C5SjbNGCdCuSayKXlLiNSNVeaGhrzFsoOkDh/QuVb4y++uF91i1bZ60Tn8= 25 | -------------------------------------------------------------------------------- /printable-characters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ansiEscapeCode = '[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]' 4 | , zeroWidthCharacterExceptNewline = '\u0000-\u0008\u000B-\u0019\u001b\u009b\u00ad\u200b\u2028\u2029\ufeff\ufe00-\ufe0f' 5 | , zeroWidthCharacter = '\n' + zeroWidthCharacterExceptNewline 6 | , zeroWidthCharactersExceptNewline = new RegExp ('(?:' + ansiEscapeCode + ')|[' + zeroWidthCharacterExceptNewline + ']', 'g') 7 | , zeroWidthCharacters = new RegExp ('(?:' + ansiEscapeCode + ')|[' + zeroWidthCharacter + ']', 'g') 8 | , partition = new RegExp ('((?:' + ansiEscapeCode + ')|[\t' + zeroWidthCharacter + '])?([^\t' + zeroWidthCharacter + ']*)', 'g') 9 | 10 | module.exports = { 11 | 12 | zeroWidthCharacters, 13 | 14 | ansiEscapeCodes: new RegExp (ansiEscapeCode, 'g'), 15 | 16 | strlen: s => Array.from (s.replace (zeroWidthCharacters, '')).length, // Array.from solves the emoji problem as described here: http://blog.jonnew.com/posts/poo-dot-length-equals-two 17 | 18 | isBlank: s => s.replace (zeroWidthCharacters, '') 19 | .replace (/\s/g, '') 20 | .length === 0, 21 | 22 | blank: s => Array.from (s.replace (zeroWidthCharactersExceptNewline, '')) // Array.from solves the emoji problem as described here: http://blog.jonnew.com/posts/poo-dot-length-equals-two 23 | .map (x => ((x === '\t') || (x === '\n')) ? x : ' ') 24 | .join (''), 25 | 26 | partition (s) { 27 | for (var m, spans = []; (partition.lastIndex !== s.length) && (m = partition.exec (s));) { spans.push ([m[1] || '', m[2]]) } 28 | partition.lastIndex = 0 // reset 29 | return spans 30 | }, 31 | 32 | first (s, n) { 33 | 34 | let result = '', length = 0 35 | 36 | for (const [nonPrintable, printable] of module.exports.partition (s)) { 37 | const text = Array.from (printable).slice (0, n - length) // Array.from solves the emoji problem as described here: http://blog.jonnew.com/posts/poo-dot-length-equals-two 38 | result += nonPrintable + text.join ('') 39 | length += text.length 40 | } 41 | 42 | return result 43 | } 44 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "printable-characters", 3 | "version": "1.0.42", 4 | "description": "A little helper for handling strings containing zero width control characters, ANSI styling, whitespaces, newlines, 💩, etc.", 5 | "main": "./build/printable-characters.js", 6 | "scripts": { 7 | "lint": "eslint printable-characters.js", 8 | "lint-test": "eslint printable-characters.js", 9 | "babel": "babel printable-characters.js --source-maps inline --out-file ./build/printable-characters.js", 10 | "build": "npm run lint && npm run lint-test && npm run babel", 11 | "test": "npm run build && env PRINTABLE_CHARACTERS_TEST_FILE=./build/printable-characters.js nyc --reporter=html --reporter=text mocha --reporter spec", 12 | "autotest": "env PRINTABLE_CHARACTERS_TEST_FILE=./printable-characters.js mocha --reporter spec --watch", 13 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/xpl/printable-characters.git" 18 | }, 19 | "keywords": [ 20 | "string width", 21 | "string length", 22 | "real string width", 23 | "real string length", 24 | "optical string width", 25 | "printed width", 26 | "unicode", 27 | "codepoints", 28 | "code points", 29 | "strlen", 30 | "zero width", 31 | "zero width symbols", 32 | "zero width characters", 33 | "visible characters", 34 | "visible symbols", 35 | "visible", 36 | "invisible", 37 | "invisible symbols", 38 | "invisible characters", 39 | "printable", 40 | "printable length", 41 | "printable symbols", 42 | "printable characters", 43 | "non-printable characters", 44 | "nonprintable characters", 45 | "characters", 46 | "symbols", 47 | "string length", 48 | "real string length", 49 | "string trimming", 50 | "trimming", 51 | "escapes", 52 | "escape codes", 53 | "codes", 54 | "ansi escapes", 55 | "tokenizing", 56 | "ansi", 57 | "whitespaces" 58 | ], 59 | "author": "Vitaly Gordon ", 60 | "license": "Unlicense", 61 | "homepage": "https://github.com/xpl/printable-characters", 62 | "devDependencies": { 63 | "babel-cli": "^6.26.0", 64 | "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", 65 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 66 | "coveralls": "^2.13.3", 67 | "eslint": "^4.8.0", 68 | "istanbul": "^0.4.5", 69 | "mocha": "^3.5.3", 70 | "nyc": "^11.2.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # printable-characters 2 | 3 | [![Build Status](https://travis-ci.org/xpl/printable-characters.svg?branch=master)](https://travis-ci.org/xpl/printable-characters) [![Coverage Status](https://coveralls.io/repos/github/xpl/printable-characters/badge.svg)](https://coveralls.io/github/xpl/printable-characters) [![npm](https://img.shields.io/npm/v/printable-characters.svg)](https://npmjs.com/package/printable-characters) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/xpl/printable-characters.svg)](https://scrutinizer-ci.com/g/xpl/printable-characters/?branch=master) [![dependencies Status](https://david-dm.org/xpl/printable-characters/status.svg)](https://david-dm.org/xpl/printable-characters) 4 | 5 | A little helper for handling strings containing zero width characters, ANSI styling, whitespaces, newlines, [weird Unicode 💩 symbols](http://blog.jonnew.com/posts/poo-dot-length-equals-two), etc. 6 | 7 | ## Determining the real (visible) length of a string 8 | 9 | ```javascript 10 | const { strlen } = require ('printable-characters') 11 | 12 | strlen ('foo bar') // === 7 13 | strlen ('\u001b[106mfoo bar\u001b[49m') // === 7 14 | ``` 15 | 16 | ## Detecting blank text 17 | 18 | ```javascript 19 | const { isBlank } = require ('printable-characters') 20 | 21 | isBlank ('foobar') // === false 22 | isBlank ('\u001b[106m \t \t \n \u001b[49m') // === true 23 | ``` 24 | 25 | ## Obtaining a blank string of the same width 26 | 27 | ```javascript 28 | const { blank } = require ('printable-characters') 29 | 30 | blank ('💩') // === ' ' 31 | blank ('foo') // === ' ' 32 | blank ('\tfoo \nfoo') // === '\t \n ' 33 | blank ('\u001b[22m\u001b[1mfoo \t\u001b[39m\u001b[22m')) // === ' \t' 34 | ``` 35 | 36 | ## Matching invisible characters 37 | 38 | ```javascript 39 | const { ansiEscapeCodes, zeroWidthCharacters } = require ('printable-characters') 40 | 41 | const s = '\u001b[106m' + 'foo' + '\n' + 'bar' + '\u001b[49m' 42 | 43 | s.replace (ansiEscapeCodes, '') // === 'foo\nbar' 44 | .replace (zeroWidthCharacters, '') // === 'foobar' 45 | ``` 46 | 47 | ## Getting the first N visible symbols, preserving the invisible parts 48 | 49 | Use for safely truncating strings to maximum width without breaking ANSI codes: 50 | 51 | ```javascript 52 | const { first } = require ('printable-characters') 53 | 54 | const s = '\u001b[22mfoobar\u001b[22m' 55 | 56 | first (s, 0) // === '\u001b[22m\u001b[22m' 57 | first (s, 1) // === '\u001b[22mf\u001b[22m' 58 | first (s, 3) // === '\u001b[22mfoo\u001b[22m' 59 | first (s, 6) // === '\u001b[22mfoobar\u001b[22m' 60 | ``` 61 | 62 | ## Extracting the invisible parts followed by the visible ones (parsing) 63 | 64 | ```javascript 65 | const { partition } = require ('printable-characters') 66 | 67 | partition ('') // [ ]) 68 | partition ('foo') // [['', 'foo'] ]) 69 | partition ('\u001b[1mfoo') // [['\u001b[1m', 'foo'] ]) 70 | partition ('\u001b[1mfoo\u0000bar') // [['\u001b[1m', 'foo'], ['\u0000', 'bar'] ]) 71 | partition ('\u001b[1mfoo\u0000bar\n') // [['\u001b[1m', 'foo'], ['\u0000', 'bar'], ['\n', '']]) 72 | ``` 73 | 74 | ## Applications 75 | 76 | - [as-table](https://github.com/xpl/as-table) — a simple function that prints objects as ASCII tables 77 | - [string.bullet](https://github.com/xpl/string.bullet) — ASCII-mode bulleting for the list-style data 78 | - [string.ify](https://github.com/xpl/string.ify) — a fancy pretty printer for the JavaScript entities 79 | - [Ololog!](https://github.com/xpl/ololog) — a better `console.log` for the log-driven debugging junkies! 80 | 81 | ## TODO 82 | 83 | Handle multi-component emojis, as in [this article](http://blog.jonnew.com/posts/poo-dot-length-equals-two): 84 | 85 | ```javascript 86 | assert.equal (strlen ('👩‍❤️‍💋‍👩'), 1) // FAILING, see http://blog.jonnew.com/posts/poo-dot-length-equals-two for possible solution 87 | assert.equal (blank ('👩‍❤️‍💋‍👩'), ' ') // FAILING, see http://blog.jonnew.com/posts/poo-dot-length-equals-two for possible solution 88 | ``` 89 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require ('assert') 4 | const printableCharacters = require (process.env.PRINTABLE_CHARACTERS_TEST_FILE) 5 | 6 | // cannot use spread operator in tests due to Node v4 compatibility requirements... 7 | const strlen = printableCharacters.strlen 8 | , isBlank = printableCharacters.isBlank 9 | , blank = printableCharacters.blank 10 | , ansiEscapeCodes = printableCharacters.ansiEscapeCodes 11 | , zeroWidthCharacters = printableCharacters.zeroWidthCharacters 12 | , partition = printableCharacters.partition 13 | , first = printableCharacters.first 14 | 15 | describe ('printable-characters', () => { 16 | 17 | it ('determines visible length', () => { 18 | 19 | assert.equal (strlen ('💩'), 1) 20 | //assert.equal (strlen ('👩‍❤️‍💋‍👩'), 1) // FAILING, see http://blog.jonnew.com/posts/poo-dot-length-equals-two for possible solution 21 | assert.equal (strlen ('❤️'), 1) 22 | assert.equal (strlen ('foo bar'), 7) 23 | assert.equal (strlen ('\u001b[106mfoo bar\u001b[49m'), 7) 24 | }) 25 | 26 | it ('detects blank text', () => { 27 | 28 | assert (!isBlank ('💩')) 29 | assert (!isBlank ('foobar')) 30 | assert ( isBlank ('\u001b[106m \t \t \n \u001b[49m')) 31 | }) 32 | 33 | it ('matches zero-width characters and ANSI escape codes', () => { 34 | 35 | let s = '\u001b[106m' + 'foo' + '\n\n' + 'bar' + '\u001b[49m' 36 | 37 | assert (s = s.replace (ansiEscapeCodes, ''), 'foo\n\nbar') 38 | assert ( s.replace (zeroWidthCharacters, ''), 'foobar') 39 | }) 40 | 41 | it ('obtains blank string of the same width', () => { 42 | 43 | assert.equal (blank ('💩'), ' ') 44 | //assert.equal (blank ('👩‍❤️‍💋‍👩'), ' ') // FAILING, see http://blog.jonnew.com/posts/poo-dot-length-equals-two for possible solution 45 | assert.equal (blank ('❤️'), ' ') 46 | assert.equal (blank ('foo'), ' ') 47 | assert.equal (blank ('\n'), '\n') 48 | assert.equal (blank ('\t'), '\t') 49 | assert.equal (blank ('\tfoo \nfoo'), '\t \n ') 50 | assert.equal (blank ('\u001b[22m\u001b[1mfoo \t\u001b[39m\u001b[22m'), ' \t') 51 | }) 52 | 53 | it ('extracts invisible parts followed by visible ones', () => { 54 | 55 | assert.deepEqual (partition (''), [ ]) 56 | assert.deepEqual (partition ('foo'), [['', 'foo'] ]) 57 | assert.deepEqual (partition ('\u001b[1mfoo'), [['\u001b[1m', 'foo'] ]) 58 | assert.deepEqual (partition ('\u001b[1mfoo\u0000bar'), [['\u001b[1m', 'foo'], ['\u0000', 'bar'] ]) 59 | assert.deepEqual (partition ('\u001b[1mfoo\u0000bar\n'), [['\u001b[1m', 'foo'], ['\u0000', 'bar'], ['\n', '']]) 60 | }) 61 | 62 | it ('gets first N visible symbols (preserving invisible parts)', () => { 63 | 64 | assert.equal (first ('💩23456789', 0), '') 65 | assert.equal (first ('💩23456789', 3), '💩23') 66 | assert.equal (first ('💩23456789', 100), '💩23456789') 67 | 68 | const s = '\u001b[22m\u001b[1m' + '💩23' + '\u0000' + '45' + '\u001b[39m' + '67' + '\n' + '89' + '\u001b[39m\u001b[22m' 69 | 70 | assert.equal (first (s, 0), '\u001b[22m\u001b[1m' + '' + '\u0000' + '' + '\u001b[39m' + '' + '\n' + '' + '\u001b[39m\u001b[22m') 71 | assert.equal (first (s, 3), '\u001b[22m\u001b[1m' + '💩23' + '\u0000' + '' + '\u001b[39m' + '' + '\n' + '' + '\u001b[39m\u001b[22m') 72 | assert.equal (first (s, 4), '\u001b[22m\u001b[1m' + '💩23' + '\u0000' + '4' + '\u001b[39m' + '' + '\n' + '' + '\u001b[39m\u001b[22m') 73 | assert.equal (first (s, 6), '\u001b[22m\u001b[1m' + '💩23' + '\u0000' + '45' + '\u001b[39m' + '6' + '\n' + '' + '\u001b[39m\u001b[22m') 74 | assert.equal (first (s, 9), '\u001b[22m\u001b[1m' + '💩23' + '\u0000' + '45' + '\u001b[39m' + '67' + '\n' + '89' + '\u001b[39m\u001b[22m') 75 | assert.equal (first (s, 100), '\u001b[22m\u001b[1m' + '💩23' + '\u0000' + '45' + '\u001b[39m' + '67' + '\n' + '89' + '\u001b[39m\u001b[22m') 76 | }) 77 | }) --------------------------------------------------------------------------------