Search code examples
javascriptsubstringword-wrap

Limit number of characters per line and wrap while preserving leading white space


My goal is:

  • Select a block of text (for this example, just a string).
  • limit number of characters per line.
  • preserve leading whitespace for each line and reapply it after text is wrapped.

I'm able to limit the number of characters per line correctly, but I'm having trouble with the white space, etc...

Any help would be appreciated

Fiddle

var str = `i am a string that has new lines and whitespace. I need to preserve the leading whitespace and add it back on after the string has been broken up after n characters.

     This line has leading whitespace. Tttttt rrrrrr  
     ttgvgggjjj. Gyjfry bh jkkfrtuj hhdt iihdrtttg.
     Here is another line. Hjkkl gggdetu jcfgjbfftt.
This line has no leading whitespace, so i dont need any reapplied. Jjjxsrg bjlkdetyhk llhfftt`;

function addNewlines(str) { 
    var result = ''; 
    while(str.length > 0) { 
          result += str.substring(0, 25) + '<br />'; 
          str = str.substring(25); 
    } 

    return result; 
}

var newStr = addNewlines(str).toString();
document.getElementById("result").innerHTML = newStr;

Should end up looking something like this:

i am a string that has ne
w lines and whitespace. I
need to preserve the lea
ding whitespace and add i
t back on after the strin
g has been broken up afte
r n characters. 

   This line has leading
   whitespace. Tttttt rr
   rrrr ttgvgggjjj. Gyjf
   ry bh jkkfrtuj hhdt i
   ihdrtttg. Here is ano
   ther line. Hjkkl gggd
   etu jcfgjbfftt. 

This line has no leading
 whitespace, so i dont n
 eed any reapplied. Jjjx
 srg bjlkdetyhk llhfftt

Solution

  • Sometimes when dealing with a new algorithm, is much easier to use two or more passes. So you think how it would work in steps instead of everything at once.

    I have an implementation working that have 2 passes: first I join the paragraph lines, and in the second pass I perform the actual splitting.

    I'm sure there's a better approach, but this works and is fully commented.

    I wasn't sure your version of ES so I made it ES5-compatible.

    https://jsfiddle.net/2ngtj3aj/

    // Divides a string into chunks of specific size
    function chunk(str, size) {
      var chunks = [];
      while(str) { 
        chunks.push(str.substring(0, size));
        str = str.substring(size);
      }
      return chunks;
    }
    
    // Removes all spaces from the left of a string
    function trimLeft(str) {
      while(str.substr(0,1) == " ") {
        str = str.substr(1);
      }
      return str;
    }
    
    // Repeats a character n times
    function repeat(c, n) {
      return Array(n + 1).join(c);
    }
    
    function addNewlines(str) { 
        var MAX_COLS = 25; // maximum colums on the text
        var DEFAULT_LEADING = 3; // default leading to reapply
        var MIN_LEADING = 1; // minimum amount of spacing to be considered a paragraph
        var CR = "\n";
        var result = '';
        var leading = 0;
        var chunks = [];
        var formattedLines = []; // store the intermediary lines
        var startLeadingSpaceLine = -1; // where does a paragrph start
        var i, l; // counters
        var lines = str.split(CR); // input lines
        
        // In the first pass, we join the paragraph lines
        
        for (i = 0; i < lines.length; i++) {
        
          l = lines[i];
          // If line is empty, we don't use it
          if (l.trim() == "") continue;
    
          if (l.substr(0, MIN_LEADING) == repeat(" ", MIN_LEADING)) {
            // If line has leading whitespace, remove the leading space
            l = trimLeft(l);
            if (startLeadingSpaceLine > -1) {
              // If we are already on a paragraph,
              // we don't overwrite the flag
            } else {
              // But if this is the first line of an paragraph,
              // We set a flag to allow to join this line with the next one
              // if that contains identation as well
              startLeadingSpaceLine = i;
            }
            // If we are on a paragraph, we don't add this line to the array,
            // first we need to wait to see if we have more lines in the paragraph
            // We also update the line in the array with the whitespace removed
            lines[i] = l;
            continue;
          } else {
            // If line doesn't has whitespace, we check if we have just finished
            // an paragraph
            if (startLeadingSpaceLine > -1) {
              // If we do, then we need to add the previous lines to the array
              // Note: if we want to leave a space between lines, we need to use
              // join(' ') instead of join('')
              var paragraphLines = lines.slice(startLeadingSpaceLine, i).join('');
              // We add the whitespace we like
              paragraphLines = repeat(" ", DEFAULT_LEADING) + paragraphLines;
              formattedLines.push(paragraphLines);
            }
            
          }
          formattedLines.push(l);
        }
        
        // Now we parse again the lines, this time we will divide
        // the lines into chunks
        
        for (i = 0; i < formattedLines.length; i++) {
        
          l = formattedLines[i];
    
          // Now check against DEFAULT_LEADAING since we have already changed
          // the identation
          if (l.substr(0, DEFAULT_LEADING) == repeat(" ", DEFAULT_LEADING)) {
            
            // If line has leading whitespace, remove the leading space
            // We aded it before just to be able to detect the paragraph.
            l = trimLeft(l);
            
            // Divide the line into chunks. We take into account the space
            // we have removed, otherwise the paragraph will bleed to the
            // right.
            l = chunk(l, MAX_COLS - DEFAULT_LEADING);
            
            // We add leading space to all paragraph lines
            for(var j = 0; j < l.length; j++) {
              l[j] = repeat(" ", DEFAULT_LEADING) + l[j];
            }
            
            // Optional: we add blank lines between paragraphs
            l = [" "].concat(l).concat([" "]);
            
          } else {
          
            // If we have a simple line, just divide it into chunks
            l = chunk(l, MAX_COLS);
    
    			}
          
          // Join the lines with newlines and add to the result
          l = l.join(CR);
          result += l + CR;
        }
        
        
        return result; 
    }
    
    var process = function() {
    	var newStr = addNewlines(input.value).toString();
    	document.getElementById("result").innerHTML = newStr;
    }
    
    var input = document.getElementById("input");
    input.addEventListener("change", process);
    input.addEventListener("keyup", process);
    process();
    <h3>RESULTS</h3>
    
    <textarea id="input" rows="10" cols="80">i am a string that has new lines and whitespace. I need to preserve the leading whitespace and add it back on after the string has been broken up after n characters.
    
         This line has leading whitespace. Tttttt rrrrrr  
         ttgvgggjjj. Gyjfry bh jkkfrtuj hhdt iihdrtttg.
         Here is another line. Hjkkl gggdetu jcfgjbfftt.
    This line has no leading whitespace, so i dont need any reapplied. Jjjxsrg bjlkdetyhk llhfftt</textarea>
    <pre id="result"></pre>