Although this question is quite old, many solutions provided so far are more complicated and expensive than necessary, as user2257198 pointed out - This is completely solvable with a short one-line regular expression.
However I found some issues with his solution including: wrapping after the max width rather than before, breaking on chars not explicitly included in the character class and not considering existing newline chars causing the start of paragraphs to be chopped mid-line.
Which led me to write my own solution:
// Static Width (Plain Regex)
const wrap = (s) => s.replace(
/(?![^\n]{1,32}$)([^\n]{1,32})\s/g, '$1\n'
);
// Dynamic Width (Build Regex)
const wrap = (s, w) => s.replace(
new RegExp(`(?![^\\n]{1,${w}}$)([^\\n]{1,${w}})\\s`, 'g'), '$1\n'
);
Bonus Features
- Handles any char that's not a newline (e.g code).
- Handles existing newlines properly (e.g paragraphs).
- Prevents pushing spaces onto beginning of newlines.
- Prevents adding unnecessary newline to end of string.
Explanation
The main concept is simply to find contiguous sequences of chars that do not contain new-lines [^\n]
, up to the desired length, e.g 32 {1,32}
. By using negation ^
in the character class it is far more permissive, avoiding missing things like punctuation that would otherwise have to be explicitly added:
str.replace(/([^\n]{1,32})/g, '[$1]\n');
// Matches wrapped in [] to help visualise
"[Lorem ipsum dolor sit amet, cons]
[ectetur adipiscing elit, sed do ]
[eiusmod tempor incididunt ut lab]
[ore et dolore magna aliqua.]
"
So far this only slices the string at exactly 32 chars. It works because it's own newline insertions mark the start of each sequence after the first.
To break on words, a qualifier is needed after the greedy quantifier {1,32}
to prevent it from choosing sequences ending in the middle of a word. A word-break char \b
can cause spaces at the start of new lines, so a white-space char \s
must be used instead. It must also be placed outside the group so it's eaten, to prevent increasing the max width by 1 char:
str.replace(/([^\n]{1,32})\s/g, '[$1]\n');
// Matches wrapped in [] to help visualise
"[Lorem ipsum dolor sit amet,]
[consectetur adipiscing elit, sed]
[do eiusmod tempor incididunt ut]
[labore et dolore magna]
aliqua."
Now it breaks on words before the limit, but the last word and period was not matched in the last sequence because there is no terminating space.
An "or end-of-string" option (\s|$)
could be added to the white-space to extend the match, but it would be even better to prevent matching the last line at all because it causes an unnecessary new-line to be inserted at the end. To achieve this a negative look-ahead of exactly the same sequence can be added before, but using an end-of-string char instead of a white-space char:
str.replace(/(?![^\n]{1,32}$)([^\n]{1,32})\s/g, '[$1]\n');
// Matches wrapped in [] to help visualise
"[Lorem ipsum dolor sit amet,]
[consectetur adipiscing elit, sed]
[do eiusmod tempor incididunt ut]
labore et dolore magna aliqua."
n
characters? – Alenealenson