From d137a66eb15ba0b27179260a730affab5e02de11 Mon Sep 17 00:00:00 2001 From: Brad Howes Date: Mon, 9 Apr 2018 15:50:21 +0200 Subject: [PATCH] Add support for a custom highlighter function. Existing code works fine when Prismjs is run in a browser to colorize code. Howver, for static site builders such as Metalsmith, we would like to not rely on browser Javascript. Setting an appopriate highlighter method on a notebookjs object allows us to continue the rendering process using Prismjs to highlight code and error cells. --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- notebook.js | 7 ++++--- notebook.min.js | 2 +- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4c9cbaf..7ac7db3 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,49 @@ To support other Markdown or ANSI-coloring engines, set `nb.markdown` and/or `nb ## Code-Highlighting -Notebook.js plays well with code-highlighting libraries. See [NBPreview](https://github.com/jsvine/nbpreview) for an example of how to add support for your preferred highlighter. +Notebook.js plays well with code-highlighting libraries. See [NBPreview](https://github.com/jsvine/nbpreview) +for an example of how to add support for your preferred highlighter. However, if you wish to inject your own +highlighting, you can install a custom highlighter function by adding it under the `highlighter` name in an +`notebookjs` instance. For instance, here is an implementation which colorizes languages using +[Prismjs](http://prismjs.com) during page generation for a static site: + +```js +var Prism = require('prismjs'); + +var highlighter = function(code, lang) { + if (typeof lang === 'undefined') lang = 'markup'; + + if (!Prism.languages.hasOwnProperty(lang)) { + try { + require('prismjs/components/prism-' + lang + '.js'); + } catch (e) { + console.warn('** failed to load Prism lang: ' + lang); + Prism.languages[lang] = false; + } + } + + return Prism.languages[lang] ? Prism.highlight(code, Prism.languages[lang]) : code; +}; + +var nb = require("notebookjs"); +nb.highlighter = function(text, pre, code, lang) { + var language = lang || 'text'; + pre.className = 'language-' + language; + if (typeof code != 'undefined') { + code.className = 'language-' + language; + } + return highlighter(text, language); + }; +``` + +A `highlighter` function takes up to four arguments: + +* `text` -- text of the cell to highlight +* `pre` -- the DOM `
` node that holds the cell
+* `code` -- the DOM `` node that holds the cell (if `undefined` then text is not code)
+* `lang` -- the language of the code in the cell (if `undefined` then text is not code)
+
+The function should at least return the original `text` value if it cannot perform any highlighting.
 
 ## MathJax 
 
diff --git a/notebook.js b/notebook.js
index 38fd75d..982e33f 100644
--- a/notebook.js
+++ b/notebook.js
@@ -58,6 +58,7 @@
         prefix: "nb-",
         markdown: getMarkdown() || ident,
         ansi: getAnsi() || ident,
+        highlighter: ident,
         VERSION: VERSION
     };
 
@@ -81,7 +82,7 @@
         var lang = this.cell.raw.language || m.language || m.language_info.name;
         code_el.setAttribute("data-language", lang);
         code_el.className = "lang-" + lang;
-        code_el.innerHTML = escapeHTML(joinText(this.raw));
+        code_el.innerHTML = nb.highlighter(escapeHTML(joinText(this.raw)), pre_el, code_el, lang);
         pre_el.appendChild(code_el);
         holder.appendChild(pre_el);
         this.el = holder;
@@ -169,7 +170,7 @@
     var render_error = function () {
         var el = makeElement("pre", [ "pyerr" ]);
         var raw = this.raw.traceback.join("\n");
-        el.innerHTML = nb.ansi(escapeHTML(raw));
+        el.innerHTML = nb.highlighter(nb.ansi(escapeHTML(raw)), el);
         return el;
     };
 
@@ -188,7 +189,7 @@
         "stream": function () {
             var el = makeElement("pre", [ (this.raw.stream || this.raw.name) ]);
             var raw = joinText(this.raw.text);
-            el.innerHTML = nb.ansi(escapeHTML(raw));
+            el.innerHTML = nb.highlighter(nb.ansi(escapeHTML(raw)), el);
             return el;
         }
     };
diff --git a/notebook.min.js b/notebook.min.js
index 1c0e5a3..1330eb8 100644
--- a/notebook.min.js
+++ b/notebook.min.js
@@ -1 +1 @@
-(function(){var root=this;var VERSION="0.3.0";var doc=root.document;if(!doc){var jsdom=require("jsdom");doc=(new jsdom.JSDOM).window.document}var ident=function(x){return x};var makeElement=function(tag,classNames){var el=doc.createElement(tag);el.className=(classNames||[]).map(function(cn){return nb.prefix+cn}).join(" ");return el};var escapeHTML=function(raw){var replaced=raw.replace(//g,">");return replaced};var joinText=function(text){if(text.join){return text.map(joinText).join("")}else{return text}};var condRequire=function(module_name){return typeof require==="function"&&require(module_name)};var getMarkdown=function(){return root.marked||condRequire("marked")};var getAnsi=function(){var req=condRequire("ansi_up");var lib=root.ansi_up||req;return lib&&lib.ansi_to_html};var nb={prefix:"nb-",markdown:getMarkdown()||ident,ansi:getAnsi()||ident,VERSION:VERSION};nb.Input=function(raw,cell){this.raw=raw;this.cell=cell};nb.Input.prototype.render=function(){if(!this.raw.length){return makeElement("div")}var holder=makeElement("div",["input"]);var cell=this.cell;if(typeof cell.number==="number"){holder.setAttribute("data-prompt-number",this.cell.number)}var pre_el=makeElement("pre");var code_el=makeElement("code");var notebook=cell.worksheet.notebook;var m=notebook.metadata;var lang=this.cell.raw.language||m.language||m.language_info.name;code_el.setAttribute("data-language",lang);code_el.className="lang-"+lang;code_el.innerHTML=escapeHTML(joinText(this.raw));pre_el.appendChild(code_el);holder.appendChild(pre_el);this.el=holder;return holder};var imageCreator=function(format){return function(data){var el=makeElement("img",["image-output"]);el.src="data:image/"+format+";base64,"+joinText(data).replace(/\n/g,"");return el}};nb.display={};nb.display.text=function(text){var el=makeElement("pre",["text-output"]);el.innerHTML=escapeHTML(joinText(text));return el};nb.display["text/plain"]=nb.display.text;nb.display.html=function(html){var el=makeElement("div",["html-output"]);el.innerHTML=joinText(html);return el};nb.display["text/html"]=nb.display.html;nb.display.marked=function(md){return nb.display.html(nb.markdown(joinText(md)))};nb.display["text/markdown"]=nb.display.marked;nb.display.svg=function(svg){var el=makeElement("div",["svg-output"]);el.innerHTML=joinText(svg);return el};nb.display["text/svg+xml"]=nb.display.svg;nb.display["image/svg+xml"]=nb.display.svg;nb.display.latex=function(latex){var el=makeElement("div",["latex-output"]);el.innerHTML=joinText(latex);return el};nb.display["text/latex"]=nb.display.latex;nb.display.javascript=function(js){var el=makeElement("script");el.innerHTML=joinText(js);return el};nb.display["application/javascript"]=nb.display.javascript;nb.display.png=imageCreator("png");nb.display["image/png"]=nb.display.png;nb.display.jpeg=imageCreator("jpeg");nb.display["image/jpeg"]=nb.display.jpeg;nb.display_priority=["png","image/png","jpeg","image/jpeg","svg","image/svg+xml","text/svg+xml","html","text/html","text/markdown","latex","text/latex","javascript","application/javascript","text","text/plain"];var render_display_data=function(){var o=this;var formats=nb.display_priority.filter(function(d){return o.raw.data?o.raw.data[d]:o.raw[d]});var format=formats[0];if(format){if(nb.display[format]){return nb.display[format](o.raw[format]||o.raw.data[format])}}return makeElement("div",["empty-output"])};var render_error=function(){var el=makeElement("pre",["pyerr"]);var raw=this.raw.traceback.join("\n");el.innerHTML=nb.ansi(escapeHTML(raw));return el};nb.Output=function(raw,cell){this.raw=raw;this.cell=cell;this.type=raw.output_type};nb.Output.prototype.renderers={display_data:render_display_data,execute_result:render_display_data,pyout:render_display_data,pyerr:render_error,error:render_error,stream:function(){var el=makeElement("pre",[this.raw.stream||this.raw.name]);var raw=joinText(this.raw.text);el.innerHTML=nb.ansi(escapeHTML(raw));return el}};nb.Output.prototype.render=function(){var outer=makeElement("div",["output"]);if(typeof this.cell.number==="number"){outer.setAttribute("data-prompt-number",this.cell.number)}var inner=this.renderers[this.type].call(this);outer.appendChild(inner);this.el=outer;return outer};nb.coalesceStreams=function(outputs){if(!outputs.length){return outputs}var last=outputs[0];var new_outputs=[last];outputs.slice(1).forEach(function(o){if(o.raw.output_type==="stream"&&last.raw.output_type==="stream"&&o.raw.stream===last.raw.stream){last.raw.text=last.raw.text.concat(o.raw.text)}else{new_outputs.push(o);last=o}});return new_outputs};nb.Cell=function(raw,worksheet){var cell=this;cell.raw=raw;cell.worksheet=worksheet;cell.type=raw.cell_type;if(cell.type==="code"){cell.number=raw.prompt_number>-1?raw.prompt_number:raw.execution_count;var source=raw.input||[raw.source];cell.input=new nb.Input(source,cell);var raw_outputs=(cell.raw.outputs||[]).map(function(o){return new nb.Output(o,cell)});cell.outputs=nb.coalesceStreams(raw_outputs)}};nb.Cell.prototype.renderers={markdown:function(){var el=makeElement("div",["cell","markdown-cell"]);el.innerHTML=nb.markdown(joinText(this.raw.source));return el},heading:function(){var el=makeElement("h"+this.raw.level,["cell","heading-cell"]);el.innerHTML=joinText(this.raw.source);return el},raw:function(){var el=makeElement("div",["cell","raw-cell"]);el.innerHTML=joinText(this.raw.source);return el},code:function(){var cell_el=makeElement("div",["cell","code-cell"]);cell_el.appendChild(this.input.render());var output_els=this.outputs.forEach(function(o){cell_el.appendChild(o.render())});return cell_el}};nb.Cell.prototype.render=function(){var el=this.renderers[this.type].call(this);this.el=el;return el};nb.Worksheet=function(raw,notebook){var worksheet=this;this.raw=raw;this.notebook=notebook;this.cells=raw.cells.map(function(c){return new nb.Cell(c,worksheet)});this.render=function(){var worksheet_el=makeElement("div",["worksheet"]);worksheet.cells.forEach(function(c){worksheet_el.appendChild(c.render())});this.el=worksheet_el;return worksheet_el}};nb.Notebook=function(raw,config){var notebook=this;this.raw=raw;this.config=config;var meta=this.metadata=raw.metadata;this.title=meta.title||meta.name;var _worksheets=raw.worksheets||[{cells:raw.cells}];this.worksheets=_worksheets.map(function(ws){return new nb.Worksheet(ws,notebook)});this.sheet=this.worksheets[0]};nb.Notebook.prototype.render=function(){var notebook_el=makeElement("div",["notebook"]);this.worksheets.forEach(function(w){notebook_el.appendChild(w.render())});this.el=notebook_el;return notebook_el};nb.parse=function(nbjson,config){return new nb.Notebook(nbjson,config)};if(typeof define==="function"&&define.amd){define(function(){return nb})}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=nb}exports.nb=nb}else{root.nb=nb}}).call(this);
+(function(){var t=this,e=t.document;if(!e){var r=require("jsdom");e=(new r.JSDOM).window.document}var n,i,a=function(t){return t},s=function(t,r){var n=e.createElement(t);return n.className=(r||[]).map(function(t){return l.prefix+t}).join(" "),n},u=function(t){return t.replace(//g,">")},p=function(t){return t.join?t.map(p).join(""):t},o=function(t){return"function"==typeof require&&require(t)},l={prefix:"nb-",markdown:t.marked||o("marked")||a,ansi:(n=o("ansi_up"),i=t.ansi_up||n,i&&i.ansi_to_html||a),highlighter:a,VERSION:"0.3.0"};l.Input=function(t,e){this.raw=t,this.cell=e},l.Input.prototype.render=function(){if(!this.raw.length)return s("div");var t=s("div",["input"]),e=this.cell;"number"==typeof e.number&&t.setAttribute("data-prompt-number",this.cell.number);var r=s("pre"),n=s("code"),i=e.worksheet.notebook.metadata,a=this.cell.raw.language||i.language||i.language_info.name;return n.setAttribute("data-language",a),n.className="lang-"+a,n.innerHTML=l.highlighter(u(p(this.raw)),r,n,a),r.appendChild(n),t.appendChild(r),this.el=t,t};var d=function(t){return function(e){var r=s("img",["image-output"]);return r.src="data:image/"+t+";base64,"+p(e).replace(/\n/g,""),r}};l.display={},l.display.text=function(t){var e=s("pre",["text-output"]);return e.innerHTML=u(p(t)),e},l.display["text/plain"]=l.display.text,l.display.html=function(t){var e=s("div",["html-output"]);return e.innerHTML=p(t),e},l.display["text/html"]=l.display.html,l.display.marked=function(t){return l.display.html(l.markdown(p(t)))},l.display["text/markdown"]=l.display.marked,l.display.svg=function(t){var e=s("div",["svg-output"]);return e.innerHTML=p(t),e},l.display["text/svg+xml"]=l.display.svg,l.display["image/svg+xml"]=l.display.svg,l.display.latex=function(t){var e=s("div",["latex-output"]);return e.innerHTML=p(t),e},l.display["text/latex"]=l.display.latex,l.display.javascript=function(t){var e=s("script");return e.innerHTML=p(t),e},l.display["application/javascript"]=l.display.javascript,l.display.png=d("png"),l.display["image/png"]=l.display.png,l.display.jpeg=d("jpeg"),l.display["image/jpeg"]=l.display.jpeg,l.display_priority=["png","image/png","jpeg","image/jpeg","svg","image/svg+xml","text/svg+xml","html","text/html","text/markdown","latex","text/latex","javascript","application/javascript","text","text/plain"];var c=function(){var t=this,e=l.display_priority.filter(function(e){return t.raw.data?t.raw.data[e]:t.raw[e]})[0];return e&&l.display[e]?l.display[e](t.raw[e]||t.raw.data[e]):s("div",["empty-output"])},h=function(){var t=s("pre",["pyerr"]),e=this.raw.traceback.join("\n");return t.innerHTML=l.highlighter(l.ansi(u(e)),t),t};l.Output=function(t,e){this.raw=t,this.cell=e,this.type=t.output_type},l.Output.prototype.renderers={display_data:c,execute_result:c,pyout:c,pyerr:h,error:h,stream:function(){var t=s("pre",[this.raw.stream||this.raw.name]),e=p(this.raw.text);return t.innerHTML=l.highlighter(l.ansi(u(e)),t),t}},l.Output.prototype.render=function(){var t=s("div",["output"]);"number"==typeof this.cell.number&&t.setAttribute("data-prompt-number",this.cell.number);var e=this.renderers[this.type].call(this);return t.appendChild(e),this.el=t,t},l.coalesceStreams=function(t){if(!t.length)return t;var e=t[0],r=[e];return t.slice(1).forEach(function(t){"stream"===t.raw.output_type&&"stream"===e.raw.output_type&&t.raw.stream===e.raw.stream?e.raw.text=e.raw.text.concat(t.raw.text):(r.push(t),e=t)}),r},l.Cell=function(t,e){var r=this;if(r.raw=t,r.worksheet=e,r.type=t.cell_type,"code"===r.type){r.number=t.prompt_number>-1?t.prompt_number:t.execution_count;var n=t.input||[t.source];r.input=new l.Input(n,r);var i=(r.raw.outputs||[]).map(function(t){return new l.Output(t,r)});r.outputs=l.coalesceStreams(i)}},l.Cell.prototype.renderers={markdown:function(){var t=s("div",["cell","markdown-cell"]);return t.innerHTML=l.markdown(p(this.raw.source)),t},heading:function(){var t=s("h"+this.raw.level,["cell","heading-cell"]);return t.innerHTML=p(this.raw.source),t},raw:function(){var t=s("div",["cell","raw-cell"]);return t.innerHTML=p(this.raw.source),t},code:function(){var t=s("div",["cell","code-cell"]);t.appendChild(this.input.render());this.outputs.forEach(function(e){t.appendChild(e.render())});return t}},l.Cell.prototype.render=function(){var t=this.renderers[this.type].call(this);return this.el=t,t},l.Worksheet=function(t,e){var r=this;this.raw=t,this.notebook=e,this.cells=t.cells.map(function(t){return new l.Cell(t,r)}),this.render=function(){var t=s("div",["worksheet"]);return r.cells.forEach(function(e){t.appendChild(e.render())}),this.el=t,t}},l.Notebook=function(t,e){var r=this;this.raw=t,this.config=e;var n=this.metadata=t.metadata;this.title=n.title||n.name;var i=t.worksheets||[{cells:t.cells}];this.worksheets=i.map(function(t){return new l.Worksheet(t,r)}),this.sheet=this.worksheets[0]},l.Notebook.prototype.render=function(){var t=s("div",["notebook"]);return this.worksheets.forEach(function(e){t.appendChild(e.render())}),this.el=t,t},l.parse=function(t,e){return new l.Notebook(t,e)},"function"==typeof define&&define.amd&&define(function(){return l}),"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=l),exports.nb=l):t.nb=l}).call(this);
\ No newline at end of file