From f16f9437b94f91b7f592ee267dd51bbdbcd7444b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20M=C3=A5rtensson?= Date: Sun, 23 Jul 2017 00:36:15 +0200 Subject: [PATCH] Add support for surrogate pairs and full width characters (#20) --- index.js | 62 ++++++++++++++++++++++++++++++++++---------------------- test.js | 9 ++++---- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/index.js b/index.js index fc1919b..3587c5d 100755 --- a/index.js +++ b/index.js @@ -2,10 +2,10 @@ const stringWidth = require('string-width'); const stripAnsi = require('strip-ansi'); -const ESCAPES = [ +const ESCAPES = new Set([ '\u001B', '\u009B' -]; +]); const END_CODE = 39; @@ -37,7 +37,7 @@ const ESCAPE_CODES = new Map([ [47, 49] ]); -const wrapAnsi = code => `${ESCAPES[0]}[${code}m`; +const wrapAnsi = code => `${ESCAPES.values().next().value}[${code}m`; // Calculate the length of words split on ' ', ignoring // the extra characters added by ansi escape codes @@ -45,18 +45,27 @@ const wordLengths = str => str.split(' ').map(s => stringWidth(s)); // Wrap a long word across multiple rows // Ansi escape codes do not count towards length -function wrapWord(rows, word, cols) { +const wrapWord = (rows, word, cols) => { + const arr = Array.from(word); + let insideEscape = false; - let visible = stripAnsi(rows[rows.length - 1]).length; + let visible = stringWidth(stripAnsi(rows[rows.length - 1])); - for (let i = 0; i < word.length; i++) { - const x = word[i]; + for (const item of arr.entries()) { + const i = item[0]; + const char = item[1]; + const charLength = stringWidth(char); - rows[rows.length - 1] += x; + if (visible + charLength <= cols) { + rows[rows.length - 1] += char; + } else { + rows.push(char); + visible = 0; + } - if (ESCAPES.indexOf(x) !== -1) { + if (ESCAPES.has(char)) { insideEscape = true; - } else if (insideEscape && x === 'm') { + } else if (insideEscape && char === 'm') { insideEscape = false; continue; } @@ -65,9 +74,9 @@ function wrapWord(rows, word, cols) { continue; } - visible++; + visible += charLength; - if (visible >= cols && i < word.length - 1) { + if (visible === cols && i < arr.length - 1) { rows.push(''); visible = 0; } @@ -78,7 +87,7 @@ function wrapWord(rows, word, cols) { if (!visible && rows[rows.length - 1].length > 0 && rows.length > 1) { rows[rows.length - 2] += rows.pop(); } -} +}; // The wrap-ansi module can be invoked // in either 'hard' or 'soft' wrap mode @@ -87,7 +96,7 @@ function wrapWord(rows, word, cols) { // than cols characters // // 'soft' allows long words to expand past the column length -function exec(str, cols, opts) { +const exec = (str, cols, opts) => { const options = opts || {}; let pre = ''; @@ -98,7 +107,10 @@ function exec(str, cols, opts) { const words = str.split(' '); const rows = ['']; - for (let i = 0, word; (word = words[i]) !== undefined; i++) { + for (const item of Array.from(words).entries()) { + const i = item[0]; + const word = item[1]; + let rowLength = stringWidth(rows[rows.length - 1]); if (rowLength) { @@ -135,33 +147,35 @@ function exec(str, cols, opts) { pre = rows.map(x => x.trim()).join('\n'); - for (let j = 0; j < pre.length; j++) { - const y = pre[j]; + for (const item of Array.from(pre).entries()) { + const i = item[0]; + const char = item[1]; - ret += y; + ret += char; - if (ESCAPES.indexOf(y) !== -1) { - const code = parseFloat(/\d[^m]*/.exec(pre.slice(j, j + 4))); + if (ESCAPES.has(char)) { + const code = parseFloat(/\d[^m]*/.exec(pre.slice(i, i + 4))); escapeCode = code === END_CODE ? null : code; } - const code = ESCAPE_CODES.get(parseInt(escapeCode, 10)); + const code = ESCAPE_CODES.get(Number(escapeCode)); if (escapeCode && code) { - if (pre[j + 1] === '\n') { + if (pre[i + 1] === '\n') { ret += wrapAnsi(code); - } else if (y === '\n') { + } else if (char === '\n') { ret += wrapAnsi(escapeCode); } } } return ret; -} +}; // For each newline, invoke the method separately module.exports = (str, cols, opts) => { return String(str) + .normalize() .split('\n') .map(line => exec(line, cols, opts)) .join('\n'); diff --git a/test.js b/test.js index a87b4e5..40af1e6 100755 --- a/test.js +++ b/test.js @@ -86,12 +86,11 @@ test('no word-wrapping', t => { t.is(res3, 'The q\nuick\nbrown\n\u001B[31mfox j\u001B[39m\n\u001B[31mumped\u001B[39m\n\u001B[31mover\u001B[39m\n\u001B[31m\u001B[39mthe l\nazy \u001B[32md\u001B[39m\n\u001B[32mog an\u001B[39m\n\u001B[32md the\u001B[39m\n\u001B[32mn ran\u001B[39m\n\u001B[32maway\u001B[39m\n\u001B[32mwith\u001B[39m\n\u001B[32mthe u\u001B[39m\n\u001B[32mnicor\u001B[39m\n\u001B[32mn.\u001B[39m'); }); -// https://github.com/chalk/wrap-ansi/issues/10 -test.failing('supports fullwidth characters', t => { +test('supports fullwidth characters', t => { t.is(m('안녕하세', 4, {hard: true}), '안녕\n하세'); }); -// https://github.com/chalk/wrap-ansi/issues/11 -test.failing('supports unicode surrogate pairs', t => { - t.is(m('a\ud83c\ude00bc', 2, {hard: true}), 'a\n\ud83c\ude00\nbc'); +test('supports unicode surrogate pairs', t => { + t.is(m('a\uD83C\uDE00bc', 2, {hard: true}), 'a\n\uD83C\uDE00\nbc'); + t.is(m('a\uD83C\uDE00bc\uD83C\uDE00d\uD83C\uDE00', 2, {hard: true}), 'a\n\uD83C\uDE00\nbc\n\uD83C\uDE00\nd\n\uD83C\uDE00'); });