Skywalker13

Diary of an ex-GeeXboX developer…

Wordwrap in javascript

Posted at — Feb 28, 2016

Wordwrap for console outputs

I need a wordwrap because I want to align the text (mostly build tools outputs) on a specific colon on the screen. It’s easy with a bit of regex.

const regex = /(.{1,19}[ \n/\\]|.{20})/g;

It wraps if there is at least a character of this list ' ', '\n', '/' or '\\' otherwise it cuts at the position 20.

Stupid example:

Build output for project: /home/foobar/devel/my_project
Compile /home/foobar/devel/my_project/toto.c
Compile /home/foobar/devel/my_project/lib/blabla.c

With a wrap for 20 chars max:

Build output for
project: /home/
foobar/devel/
my_project

Compile /home/
foobar/devel/
my_project/toto.c

Compile /home/
foobar/devel/
my_project/lib/
blabla.c

It’s very simple and the job is mostly done. Mostly because there is an other case where it’s a bit more difficult. I like colors in my outputs but I will preserve a correct wordwrap.

The same example with ANSI colors:

\u001b[31mBuild\u001b[0m output for project: \u001b[32m/home/foobar/devel/my_project\u001b[0m
\u001b[31mCompile\u001b[0m \u001b[32m/home/foobar/devel/my_project/toto.c\u001b[0m
\u001b[31mCompile\u001b[0m \u001b[32m/home/foobar/devel/my_project/lib/blabla.c\u001b[0m

But the result is weird because the colors should not be considered in the regex:

\u001b[31mBuild\
u001b[0m output for
project: \u001b[32m/
home/foobar/devel/
my_project\u001b[0m

\u001b[31mCompile\
u001b[0m \u001b[32m/
home/foobar/devel/
my_project/toto.c\
u001b[0m

\u001b[31mCompile\
u001b[0m \u001b[32m/
home/foobar/devel/
my_project/lib/
blabla.c\u001b[0m

The visible result:

Build
 output for
project: /
home/foobar/devel/
my_project

Compile\
 /
home/foobar/devel/
my_project/toto.c\


Compile\

home/foobar/devel/
my_project/lib/
blabla.c

Handling ANSI colors

The idea is to find all color positions in order to restore the patterns after the wordwrap. It means that the wordwrap must be done only on a text without ANSI colors.

Here the regex to strip the ANSI colors:

const regexAnsi = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;

It comes from ansi-regex. Then it’s possible to generate an array of position with our original text.

const ansiRegex = require("ansi-regex");

function colorIndexes(text) {
  const regex = ansiRegex();
  const list = [];
  let res;
  while ((res = regex.exec(text))) {
    list.push({
      color: res[0],
      index: res.index,
    });
  }
  return list;
}

const colors = colorIndexes(text);

Here the output with our text:

[
  { color: "\u001b[31m", index: 0 },
  { color: "\u001b[0m", index: 10 },
  { color: "\u001b[32m", index: 35 },
  { color: "\u001b[0m", index: 69 },
  { color: "\u001b[31m", index: 74 },
  { color: "\u001b[0m", index: 86 },
  { color: "\u001b[32m", index: 91 },
  { color: "\u001b[0m", index: 132 },
  { color: "\u001b[31m", index: 137 },
  { color: "\u001b[0m", index: 149 },
  { color: "\u001b[32m", index: 154 },
  { color: "\u001b[0m", index: 201 },
];

Then we can strip the ANSI colors, apply the regex wordwrap and restore the colors.

Strip the ANSI colors

text = text.replace(ansiRegex(), "");

Apply the regex worwrap

let output = "";
const regex = /(.{1,19}[ \n/\\]|.{20})/g;
const matches = text.match(regex) || [text];
matches.forEach((part, index) => {
  output += part;

  if (index < matches.length - 1) {
    output += "\n";
  }
});

The forEach is necessary in order to produce the output with the new linefeeds. Here we have a correct output but without the colors.

Restore the colors

const colors = colorIndexes(text);

let colorsOffset = 0;
let output = "";
const regex = /(.{1,19}[ \n/\\]|.{20})/g;
const matches = text.match(regex) || [text];
matches.forEach((part, index) => {
  output += part;

  /* Restore the colors */
  while (colors.length) {
    const offset = colors[0].index + colorsOffset;
    if (offset >= output.length) {
      break;
    }

    output = output.substr(0, offset) + colors[0].color + output.substr(offset);
    colors.shift();
  }

  if (index < matches.length - 1) {
    output += "\n";
    colorsOffset++;
  }
});

The colors are removed from the list colors step by step accordingly to the current offset.

colors