Passion & Opportunity ? continue : break

How to show line number in <pre>?
Written: 2019-04-04 09:07:12 Last update: 2019-06-16 11:17:08

To show line number inside a <pre> tag is easy, it can be displayed using pure CSS but has a small problem, I've provided a simple demo below and a workaround to use very small JS to solve pure CSS solution.

Using pure CSS counter demo

The CSS style
pre.withlinenumber {
  counter-reset: line;
}
pre.withlinenumber code {
  counter-increment: line;
  display: flex;
}
pre.withlinenumber code::before {
  content: counter(line) ' ';

  color: #eee;
  background-color: #555;
  font-family: monospace;

  /* required to anticipate for more than 2 digits (passed 99) */
  width: 2.5rem;

  /* do not allow to shrink when screen width not enough, maintain width */
  flex-shrink: 0;

  text-align: right;

  border-right: 1px solid #777;
  -webkit-user-select: none; /* avoid using mouse to select */
}
The HTML
<pre class="withlinenumber">
<code>window.addEventListener('load', function() {</code>
<code>  getLocalStylesToInject();</code>
<code></code>
<code>  getLocalJavascriptsToRun();</code>
<code></code>
<code>  formatPreWithLineNumbers();</code>
<code>});</code>
</pre>
The result
// after DOM is completely loaded then start inject local style and run local script
window.addEventListener('load', function() {
  getLocalStylesToInject();

  getLocalJavascriptsToRun();

  formatPreWithLineNumbers();
});

This pure CSS is working as expected and quite easy, but there is a slight problem with this solution, the HTML code will need to add '<code>' in the beginning of each line and also need a '</code>' at the end of each line.

Normally we use line number to display a source code, so what happend if we have more than 100 lines inside ? are we going to copy and paste '<code>' and '</code>' for each line ?

A small Javascript as workaround to solve it, the Javascript simply parse each line inside <pre class="withlinenumber"> then adds '<code>' and '</code>' for each line.

function formatPreWithLineNumbers() {
  let nodeList = document.querySelectorAll('pre.withlinenumber'); // multi classes
  if(! nodeList || nodeList.length < 1) {
    return;
  }

  let totalNode = nodeList.length;
  for(let i = 0; i < totalNode; i++) {
    let ele = nodeList[i];
    let content = ele.innerText;

    let lines = content.split(/\n/);
    let totalLine = lines.length;

    let newContent = '';

    for(let j = 0; j < totalLine; j++) {
      if(lines[j].length > 0) {
        newContent += '<code>' + lines[j] + '</code>';
      }
    }

    // replace content,
    // NOTE: use innerHTML (instead of innerText) because newContent has '<code>' tag
    ele.innerHTML = newContent;
  }
}

The CSS content will remain the same and needed for styling, the above javascript is enough but if we want to change colors for each line then we need to add more code, in this page I only add simple logic to detect:

  • Comment: line starting with '//' (double slashes)
  • Function call: any word starts with a dot '.' and ends with a '('
  • A string: starts with a single quote (') and also ends with a single quote too
Please see a little more javascript below
function formatPreWithLineNumbers() {
  var nodeList = document.querySelectorAll('pre.withlinenumber'); // multi classes
  if(! nodeList || nodeList.length < 1) {
    return;
  }

  var useColor;

  var totalNode = nodeList.length;
  for(var i = 0; i < totalNode; i++) {
    var ele = nodeList[i];

    if(ele.classList.contains('nocolor')) {
      useColor = false;
    } else {
      // default is using color
      useColor = true;
    }
    
    // 20190509: avoid double parsing
    if(ele.classList.contains('parsed')) {
      continue; // already parsed, so ignore it
    }
    ele.classList.add('parsed'); // set flag !!

    //var content = ele.innerText.trim();
    // use innerHTML to keep any HTML tag inside
    var content = ele.innerHTML.trim();

    var lines = content.split(/\n/);
    var totalLine = lines.length;

    var newContent = '';

    for(var j = 0; j < totalLine; j++) {

      // replace all '<' with '&lt;' to disable HTML tags
      var text = lines[j].replace(/</g, '&lt;');
      
      if(! useColor) {
        // dont use color, so output immediately
        newContent += '<code>' + text + '</code>';
        continue;
      }

      // search for a comment, it starts with '//' 
      if(0 == text.trim().indexOf('//')) {
        // start with '//', so it is a comment 
        text = '<i class="comment">' + text + '</i>';
      } else {
        var stringArray, totalStringArray;

        // NOTE: start replacing single character

        // replace all '='
        text = text.replace(/=/g, '<i class="equal">=</i>');

        // replace all ':'
        text = text.replace(/:/g, '<i class="colon">:</i>');
        
        // NOTE: at this point
        // nothing should check for double-quote because it is used by above "equal" and "colon" classes.
        // therefore do not search for double quote, because issue above !!!

        // find STRING: find opening single quote (') and ending single quote (')
        stringArray = text.match(/'.*?'/g);
        totalStringArray = stringArray ? stringArray.length : 0;
        if(totalStringArray > 0) {
          text = addStyle(text, stringArray, 'text');
        }
        // failed to parse line which has 3 or 5 or 7 (etc.) single quotes, pay attention here !!

        //find FUNCTION CALL: find opening single dot '.' follows by any single character of 'a-zA-Z0=9'
        // continue to ends with a bracket '('
        stringArray = text.match(/[.][a-zA-Z0-9]*[(]/g);
        totalStringArray = stringArray ? stringArray.length : 0;
        if(totalStringArray > 0) {

          // exclude if there is ' ' in the middle of this single line
          stringArray = stringArray.filter(function(val, index) {
            // use '1' to search from second character
            if(val.indexOf(' ', 1) > 0) {
              return false;
            }
            return true;
          });

          // exclude 1 char prefix '.' and 1 char suffix '('
          text = addStyle(text, stringArray, 'func', 1, 1);
        }
      }

      newContent += '<code>' + text + '</code>';
    }

    // replace content,
    // NOTE: use innerHTML (instead of innerText) because newContent has '<code>' tag
    ele.innerHTML = newContent;
  }
}

function addStyle(text, stringArray, iClass, excludePrefix, excludeSuffix) {
  var totalStringArray = stringArray ? stringArray.length : 0;
  if(totalStringArray < 1) {
    return text;
  }

  // validate value, make sure it is a valid value (>= 0)
  excludePrefix = excludePrefix && excludePrefix > 0 ? excludePrefix : 0;
  excludeSuffix = excludeSuffix && excludeSuffix > 0 ? excludeSuffix : 0;
  
  if(totalStringArray > 2) {//} || excludePrefix || excludeSuffix) {
    excludePrefix = excludePrefix; // TEST breakpoint
  }

  var previousPos = 0, currentPos, length;

  var formattedString = ''; // must init empty string

  for(var i = 0; i < totalStringArray; i++) {
    currentPos = text.indexOf(stringArray[i], previousPos);
    length = stringArray[i].length;

    if(currentPos > previousPos) {
      // have 'head'
      formattedString += text.substr(previousPos, (currentPos - previousPos) + excludePrefix);
    }

    // body
    formattedString += '<i class="' + iClass + '">' + text.substr(currentPos + excludePrefix, length - (excludePrefix + excludeSuffix)) + '</i>';

    // update 'previousPos'
    previousPos = (currentPos + length) - excludeSuffix;
  }

  // any 'tail' left ?
  if(currentPos + length < text.length) {
    formattedString += text.substr((currentPos + length) - excludeSuffix);
  }

  return formattedString;
}

This small JavaScript code is very useful, I am personally can read the content of 'pre' much better, especially if the content has so many lines of code, of course we could make it better but it means longer JavaScript and may not be worth it.