8

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.

tig
  • 3,424
  • 3
  • 32
  • 65
  • Maybe you'd find this link helpful https://github.com/PrismJS/prism/issues/1420 – mahmoudafer Jan 01 '20 at 02:52
  • Nope, that's not it. I think it's because line-numbers actually uses Javascript to modify the DOM. IOW, it *can't* work in a static page without Javascript. – tig Jan 01 '20 at 21:57
  • Yes I think that's the reason. The `Prism.highlight(..)` function deals only with highlighting specific language syntax in the source code that you provide. The line-numbers plugin acts on the blocks in the page after they are loaded on the browser. You can see from the examples given in the Prism docs that you can apply line numbers to plain text blocks that have not been through `highlight(..)`. – JohnRC Jan 02 '20 at 13:26
  • 1
    @JohnRC I have the ability to modify the HTML `highlight` generates. I wonder if I could inject the right HTML at the start of each line? I think this is what you are suggesting, but I don't see it obviously from my read of the Prism docs. Can you expand on your suggestion? – tig Jan 02 '20 at 18:48
  • you are including css right? – Diego B Jan 03 '20 at 00:42
  • do you have some custom css for pre tag? – Diego B Jan 03 '20 at 00:50
  • @tig Sorry my comment was not clear. I would rephrase: "..you can use Prism to add line numbers even on plain text blocks that have not been through `highlight(..)` which indicates that the line numbers are added by script after the page has been loaded in the browser" – JohnRC Jan 03 '20 at 12:06

3 Answers3

11

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>
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
  • 1
    Hero. It turns out litehtml does not support CSS `content: counter` so I have no choice but to use a variant of your 2nd suggestion. But your answer was the detail I was looking for. My javascript/node is so rusty, I was unable to easily decode the code in `Prism.highlight` on my own. Enjoy the bounty ;-) – tig Jan 03 '20 at 20:12
  • 1
    @tig that's a shame because the best way to do it and be copy friendly, is by using CSS; but I thought about it and I realized that you may need a way to add static line numbers directly in-front of each line. You can change the line number format, but the most important part is the indentation. Thanks for the bounty! – Christos Lytras Jan 07 '20 at 09:12
1

I had a React website with code snippets, and I used prismjs node module like that:

SourceCode.js

import * as Prism from "prismjs";
export default function SourceCode(props) {
    return (
        <div>
            <div style={{ maxWidth: 900 }}>
                <pre className="language-javascript" style={{ backgroundColor: "#272822", fontSize: "0.8em" }}>
                    <code
                        style={{ fontFamily: "Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace" }}
                        dangerouslySetInnerHTML={{
                            __html: Prism.highlight(props.code, Prism.languages.javascript, "javascript"),
                        }}
                    />
                </pre>
            </div>
        </div>
    );
};

Then I decided to add line-numbers plugin and I had hard time figuring out how to make it work with Node.js and React. The problem was that line-numbers used DOM, and one does not simply use DOM in Node.js.

What I finally did. I uninstalled prismjs module and did it in an old fashion way :).

index.html

<html lang="en-us">
    <head>
        <meta charset="utf-8" />
        <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
        <title>SciChart Web Demo</title>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.21.0/themes/prism-okaidia.min.css" />
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.21.0/plugins/line-numbers/prism-line-numbers.min.css" />
        <script async type="text/javascript" src="bundle.js"></script>
    </head>
    <body>
        <div id="react-root"></div>
        <script>
            window.Prism = window.Prism || {};
            Prism.manual = true;
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.21.0/prism.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.21.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
    </body>
</html>

SourceCode.js

import * as React from "react";

export default function SourceCode(props) {
    React.useEffect(() => {
        window.Prism.highlightAll();
    }, []);
    return (
        <div>
            <div style={{ maxWidth: 900 }}>
                <pre
                    className="language-javascript line-numbers"
                    style={{ backgroundColor: "#272822", fontSize: "0.8em" }}
                >
                    <code style={{ fontFamily: "Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace" }}>
                        {props.code}
                    </code>
                </pre>
            </div>
        </div>
    );
};
Michael Klishevich
  • 1,774
  • 1
  • 17
  • 17
0

Maybe this help you:

Codepen with error:

link

(the codepen lines exceed the limit of stack-overflow !)

enter image description here

Codepen working OK:

link

(the codepen lines exceed the limit of stack-overflow !)

enter image description here

Changes between both:

pre.line-numbers {
 position: relative;
 padding-left: 3.8em;
 counter-reset: linenumber;
}

pre.line-numbers>code {
 position: relative;
}

.line-numbers .line-numbers-rows {
 position: absolute;
 pointer-events: none;
 top: 0;
 font-size: 100%;
 left: -3.8em;
 width: 3em;
 /* works for line-numbers below 1000 lines */
 letter-spacing: -1px;
 border-right: 1px solid #999;
 -webkit-user-select: none;
 -moz-user-select: none;
 -ms-user-select: none;
 user-select: none;
}

.line-numbers-rows>span {
 pointer-events: none;
 display: block;
 counter-increment: linenumber;
}

.line-numbers-rows>span:before {
 content: counter(linenumber);
 color: #999;
 display: block;
 padding-right: 0.8em;
 text-align: right;
}
Diego B
  • 1,256
  • 11
  • 19
  • This does not appear to work if you disable Javascript, which is what I need (the html renderer I'm using does not support Javascript). – tig Jan 03 '20 at 01:43
  • 1
    a that ("the html renderer I'm using does not support Javascript") is a good point to know about... – Diego B Jan 03 '20 at 01:46
  • 1
    When I wrote "static html files" above, I thought that was clear. But, I can see how it's ambiguous. I've updated my question to be more clear. I appreciate your attempt to help though. – tig Jan 03 '20 at 02:23