marc walter

Using Prism with marked for static syntax highlighting

2017-09-27

This content is built from markdown files to static html. Syntax highlighting for code snippets is also generated during this step. It is very easy to add syntax-highlighting only in the browser, but I wanted to remove the necessary JavaScript dependencies from the HTML code and the additional rendering effort for the browser.

Example

let ab = { cde: 1, fg: 'h' }
console.log(`Variable ab = ${ab}`)

// comment
ab.i = 17

For the Redesign I decided to switch from highlightjs to Prism. This post lists the steps to achieve build-time syntax highlighting for many languages using Prism and node.js.

Add a highlight function that uses Prism

For this I needed a custom highlight function, as described in this techy issue.

const marked = require('marked');
const Prism = require('prismjs');

console.log('Available Prism languages', JSON.stringify(Object.keys(Prism.languages)))

marked.setOptions({
    highlight: (code, lang) => {
        if (!Prism.languages[lang]) {
            const fallback = 'markup'
            console.error(`Language '${lang}' is not available in Prism.js, will use '${fallback}' instead.`)
            lang = fallback
        }
        return Prism.highlight(code, Prism.languages[lang])
    }
})

const html = marked('# Sample\n```javascript\nalert('hello');\n```')

This seems to work, but unfortunately Prism expects slightly different markup.

Marked will wrap the highlighted code in <pre><code class="lang-javascript">[...]</code></pre> But Prism.js needs this markup: <pre class="language-javascript"><code class="language-javascript">[...]</code></pre>

Add a custom marked renderer for code blocks

For that I wrote a custom renderer adapting code written by Glen Cheney:

const marked = require('marked')
const Prism = require('prismjs');

const renderer = new marked.Renderer()
// wrap code block the way Prism.js expects it
renderer.code = function(code, lang, escaped) {
    code = this.options.highlight(code, lang);

    if (!lang) {
      return '<pre><code>' + code + '</code></pre>';
    }

    // e.g. "language-js"
    var langClass = 'language-' + lang;

    return '<pre class="' + langClass + '"><code class="' + langClass + '">' +
      code +
      '</code></pre>';
};

marked.setOptions({
    renderer,
    highlight: (code, lang) => {
        if (!Prism.languages[lang]) {
          const fallback = 'markup'
          console.error(`Language '${lang}' is not available in Prism.js, will use '${fallback}' instead.`)
          lang = fallback
        }
        return Prism.highlight(code, Prism.languages[lang])
    }
})

const html = marked('# Sample\n```javascript\nalert('hello');\n```')

Add more languages to Prism

Currently Prism only knows the following languages _"markup", "xml", "html", "mathml", "svg", "css", "clike", "javascript", "js"_, but I wanted more.

For this, I added

const Prism = require('prismjs')

// for more, check http://prismjs.com/index.html#languages-list
require('prismjs/components/prism-batch')
require('prismjs/components/prism-typescript')
require('prismjs/components/prism-php')
require('prismjs/components/prism-python')

/* ... */

Now prism will support the following languages: "markup", "xml", "html", "mathml", "svg", "css", "clike", "javascript", "js", "batch", "typescript", "ts", "python"

Add other Plugins to Prism

Prism comes with a variety of plugins, but currently I am not using any.
There is an issue on git on how to integrate plugins that need DOM, maybe there will be an explanation in the future.

Full script

I created a separate node module for my custom marked render.

Usage

const marked = require('./custom-marked')
config = {
    gfm: true,
    tables: true,
    breaks: false,
    pedantic: false,
    sanitize: false,
    smartLists: true,
    smartypants: false,
}

marked.setOptions(config)

const html = marked('# Sample\n```javascript\nalert('hello');\n```')

File custom-marked.js:

const marked = require('marked')
const Prism = require('prismjs')

// add languages, for more see http://prismjs.com/index.html#languages-list
require('prismjs/components/prism-batch')
require('prismjs/components/prism-typescript')
require('prismjs/components/prism-php')
require('prismjs/components/prism-python')

console.log('Available Prism languages', JSON.stringify(Object.keys(Prism.languages)))

// custom renderer and highlight function
const custom = new (function Custom() {
    this.renderer = new marked.Renderer()
    this.renderer.code = function (code, lang, escaped) {
        code = this.options.highlight(code, lang)
        if (!lang) lang = 'unknown'

        var langClass = `language-${lang}`

        return `<pre class="${langClass}"><code class="${langClass}">${code}</code></pre>`
    }

    this.highlight = function (code, lang) {
        if (!Prism.languages[lang]) {
            const fallback = 'javascript'
            console.error(`prismjs does not know language '${lang}', using '${fallback}' instead.`)
            lang = fallback
        }
        return Prism.highlight(code, Prism.languages[lang])
    }

    return this
})()

// export marked with a changed setOptions method
module.exports = marked
marked._setOptions = marked.setOptions
marked.setOptions = options => {
    // use custom renderer and highlight functions if not set from the caller
    if (!options.hasOwnProperty('renderer')) options.renderer = custom.renderer
    if (typeof options.highlight !== 'function') options.highlight = custom.highlight

    marked._setOptions(options)
}