Search code examples
node.jsprismjs

Static html generation with prismjs - how to enable line-numbers?


I'm using node.js to generate static html files from code, formatting them with prismjs. Within my app, I do not have access to an HTML renderer that supports Javascript (I'm using 'htmllite'). So I need to be able to generate HTML that does not require Javascript.

const Prism = require('prismjs');
const loadLanguages = require('prismjs/components/');
loadLanguages(['csharp']);
const code = '<a bunch of C# code>';
const html = Prism.highlight(code, Prism.languages.csharp, 'csharp');

This works great. But I want to use the line-numbers plugin and don't see how to make it work. My <pre> has the line-numbers class, and I get a bigger left margin, but no line numbers.


Solution

  • PrismJS needs DOM for most plugins to work. After looking at the code inside plugins/line-numbers/prism-line-numbers.js#L109, we can see that the line numbers is just a span element with class="line-numbers-rows" that it contains an empty span for each line. We can emulate this behavior without DOM by just using the same regular expression that prism-line-numbers uses to get the lines number and then compose a string that has the html code of the span.line-numbers-rows and add an empty string <span></span> for each line.

    Prism.highlight runs only 2 hooks, before-tokenize and after-tokenize. We'll use after-tokenize to compose a lineNumbersWrapper string that contains the span.line-numbers-rows element and the empty span line elements:

    const Prism = require('prismjs');
    const loadLanguages = require('prismjs/components/');
    loadLanguages(['csharp']);
    
    const code = `Console.WriteLine();
    Console.WriteLine("Demo: Prism line-numbers plugin with nodejs");`;
    
    // https://github.com/PrismJS/prism/blob/master/plugins/line-numbers/prism-line-numbers.js#L109
    var NEW_LINE_EXP = /\n(?!$)/g;
    var lineNumbersWrapper;
    
    Prism.hooks.add('after-tokenize', function (env) {
      var match = env.code.match(NEW_LINE_EXP);
      var linesNum = match ? match.length + 1 : 1;
      var lines = new Array(linesNum + 1).join('<span></span>');
    
      lineNumbersWrapper = `<span aria-hidden="true" class="line-numbers-rows">${lines}</span>`;
    });
    
    const formated = Prism.highlight(code, Prism.languages.csharp, 'csharp');
    const html = formated + lineNumbersWrapper;
    
    console.log(html);
    

    This will output:

    Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Demo: Generate invalid numbers"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span>
    

    which has span.line-numbers-rows at the end:

    <span aria-hidden="true" class="line-numbers-rows">
      <span></span>
      <span></span>
    </span>
    

    Now if we use that output in a pre.language-csharp.line-numbers code.language-csharp element, we'll get the proper line numbers result. Check this Codepen that has only themes/prism.css and plugins/line-numbers/prism-line-numbers.css and properly displays line numbers with the above outputted code.

    Note that each line (except the first one) has to be markup intended for the code to appear properly and that's because we're inside a pre.code block, but I guess you already know that.

    UPDATE

    In case you don't rely on CSS and you want just a line number before each line, then you can add one by splitting all the lines and add each index + 1 with a space padding at the start using padStart:

    const Prism = require('prismjs');
    const loadLanguages = require('prismjs/components/');
    loadLanguages(['csharp']);
    
    const code = `Console.WriteLine();
    Console.WriteLine("Demo: Prism line-numbers plugin with nodejs");`;
    
    const formated = Prism.highlight(code, Prism.languages.csharp, 'csharp');
    
    const html = formated
      .split('\n')
      .map((line, num) => `${(num + 1).toString().padStart(4, ' ')}. ${line}`)
      .join('\n');
    
    console.log(html);
    

    Will output:

       1. Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
       2. Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Demo: Prism line-numbers plugin with nodejs"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>