Skip to content

Commit

Permalink
feat: do syntax highlighting in web worker (closes #964) (#1060)
Browse files Browse the repository at this point in the history
* feat: do syntax highlight in web worker (closes #964)

* chore(.gitignore): ignore generated js/core/hightlight.js file

* chore(.gitignore): ignore generated js/core/worker.js file

* chore(profile-w3c-common): load hljs directly from w3c

* feat(core/worker): opportunistically preload syntax highlighter

* tests: fix tests to work with hljs worker

* refactor(core/worker): load worker.js as Blob URL

* feat(core/markdown): apply 'nolinks' on all elements
  • Loading branch information
Marcos Cáceres committed Feb 1, 2017
1 parent 37990c1 commit 701d542
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 73 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -15,10 +15,12 @@ js/core/biblio.js
js/core/data-cite.js
js/core/data-include.js
js/core/default-root-attr.js
js/core/highlight.js
js/core/include-config.js
js/core/override-configuration.js
js/core/post-process.js
js/core/pre-process.js
js/core/pubsubhub.js
js/core/remove-respec.js
js/core/respec-ready.js
js/core/worker.js
44 changes: 0 additions & 44 deletions js/core/highlight.js

This file was deleted.

4 changes: 2 additions & 2 deletions js/core/markdown.js
Expand Up @@ -270,8 +270,8 @@ define([
.replace(/\n\s*&quot;</mg, " &quot;<");
var beautifulHTML = beautify.html_beautify(cleanHTML, beautifyOps);
newBody.innerHTML = beautifulHTML;
// Remove links where class pre.nolinks
substituteWithTextNodes(newBody.querySelectorAll("pre.nolinks a[href]"));
// Remove links where class .nolinks
substituteWithTextNodes(newBody.querySelectorAll(".nolinks a[href]"));
// Restructure the document properly
var fragment = structure(newBody, doc);
// Frankenstein the whole thing back together
Expand Down
3 changes: 2 additions & 1 deletion js/profile-w3c-common.js
Expand Up @@ -16,6 +16,7 @@ require.config({
"beautify-css": "deps/beautify-css",
"beautify-html": "deps/beautify-html",
"handlebars.runtime": "deps/handlebars",
"deps/highlight": "https://www.w3.org/Tools/respec/respec-highlight",
},
deps: [
"deps/fetch",
Expand Down Expand Up @@ -51,7 +52,6 @@ define([
"core/webidl-index",
"core/webidl-oldschool",
"core/link-to-dfn",
"core/highlight",
"core/contrib",
"core/fix-headers",
"core/structure",
Expand All @@ -67,6 +67,7 @@ define([
"ui/save-html",
"ui/search-specref",
"w3c/seo",
"core/highlight",
/*Linter must be the last thing to run*/
"w3c/linter",
],
Expand Down
87 changes: 87 additions & 0 deletions src/core/highlight.js
@@ -0,0 +1,87 @@
/**
* Module core/highlight
*
* Performs syntax highlighting to all pre and code elements.
*/
import { pub, sub } from "core/pubsubhub";
import utils from "core/utils";
import { worker } from "core/worker";
import ghCss from "deps/text!core/css/github.css";
export const name = "core/highlight";

// Opportunistically insert the style into the head to reduce FOUC.
var codeStyle = document.createElement("style");
codeStyle.textContent = ghCss;
var swapStyleOwner = utils.makeOwnerSwapper(codeStyle);
swapStyleOwner(document.head);

function getLanguageHint(classList) {
return Array
.from(classList)
.filter(item => item !== "highlight")
.map(item => item.toLowerCase());
}
let doneResolver;
let doneRejector;
export const done = new Promise((resolve, reject) => {
doneResolver = resolve;
doneRejector = reject;
});

export async function run(conf, doc, cb) {
// Nothing to do
if (conf.noHighlightCSS) {
doneResolver();
return cb();
}

if (codeStyle.ownerDocument !== doc) {
swapStyleOwner(doc.head);
}

if (doc.querySelector(".highlight")) {
pub("warn", "pre elements don't need a 'highlight' class anymore.");
}

const promisesToHighlight = Array
.from(
doc.querySelectorAll("pre:not(.idl):not(.highlightdone)")
)
.map(element => {
return new Promise((resolve, reject) => {
if (element.textContent.trim() === "") {
return resolve(); // no work to do
}
const msg = {
action: "highlight",
code: element.textContent,
id: Math.random().toString(),
languages: getLanguageHint(element.classList),
};

worker.postMessage(msg);
worker.addEventListener("message", function listener(ev) {
if (ev.data.id !== msg.id) {
return; // not for us!
}
worker.removeEventListener("message", listener);
element.innerHTML = ev.data.value;
element.classList.add("hljs");
resolve();
});
setTimeout(() => {
const errMsg = "Timeout error trying to process: " + msg.code;
const err = new Error(errMsg);
reject(err);
}, 5000);
});
});
try {
await Promise.all(promisesToHighlight);
doneResolver();
} catch (err) {
console.error(err);
doneRejector(err);
}
cb();
}
21 changes: 21 additions & 0 deletions src/core/worker.js
@@ -0,0 +1,21 @@
/**
* Module core/worker
*
* Exports a Web Worker for ReSpec, allowing for
* multi-threaded processing of things.
*/

// Opportunistically preload syntax highlighter, which is used by the worker
import utils from "core/utils";
import workerScript from "deps/text!../../worker/respec-worker.js";
// Opportunistically preload syntax highlighter
const hint = {
hint: "preload",
href: "https://www.w3.org/Tools/respec/respec-highlight.js",
as: "script",
};
const link = utils.createResourceHint(hint);
document.head.appendChild(link);

const workerURL = URL.createObjectURL(new Blob([workerScript], {type : 'application/javascript'}));
export const worker = new Worker(workerURL);
47 changes: 33 additions & 14 deletions tests/spec/core/highlight-spec.js
@@ -1,19 +1,20 @@
"use strict";
describe("Core — Highlight", function() {
afterAll(function(done) {
afterAll(done => {
flushIframes();
done();
});

it("should't highlight idl blocks", function(done){
it("shouldn't highlight idl blocks", done => {
var ops = {
config: makeBasicConfig(),
body: makeDefaultBody() +
"<section><pre class=idl>"+
"[Constructor]interface Dahut : Mammal {" +
" const unsigned short DEXTROGYROUS = 1;" +
" Dahut turnAround(float angle, boolean fall);" +
"};</pre></section>"
body: makeDefaultBody() + `
<section><pre class=idl>
[Constructor]interface Dahut : Mammal {
const unsigned short DEXTROGYROUS = 1;
Dahut turnAround(float angle, boolean fall);
};</pre>
</section>`
};
makeRSDoc(ops, function(doc) {
var pre = doc.querySelector("pre");
Expand All @@ -22,11 +23,17 @@ describe("Core — Highlight", function() {
}).then(done);
});

it("should automatically highlight", function(done) {
it("should automatically highlight", done => {
var ops = {
config: makeBasicConfig(),
body: makeDefaultBody() +
"<section><pre class=example>function () {\n alert('foo');\n}</pre></section>"
`<section>
<pre class=example>
function foo() {
alert('foo');
}
</pre>
</section>`
};
makeRSDoc(ops, function(doc) {
var pre = doc.querySelector("div.example pre");
Expand All @@ -35,11 +42,17 @@ describe("Core — Highlight", function() {
}).then(done);
});

it("shouldn't highlight pre elements when told not to", function(done) {
it("shouldn't highlight pre elements when told not to", done => {
var ops = {
config: makeBasicConfig(),
body: makeDefaultBody() +
"<section><pre class='nohighlight example'>function () {\n alert('foo');\n}</pre></section>"
`<section>
<pre class='nohighlight example'>
function foo() {
alert('foo');
}
</pre>
</section>`
};
makeRSDoc(ops, function(doc) {
var pre = doc.querySelector("div.example pre");
Expand All @@ -48,11 +61,17 @@ describe("Core — Highlight", function() {
}).then(done);
});

it("should respect the noHighlightCSS by not highlighting anything", function(done) {
it("should respect the noHighlightCSS by not highlighting anything", done => {
var ops = {
config: Object.assign(makeBasicConfig(), { noHighlightCSS: true }),
body: makeDefaultBody() +
"<section><pre id=test>function () {\n alert('foo');\n}</pre></section>"
`<section>
<pre id="test">
function foo() {
alert('foo');
}
</pre>
</section>`
};
makeRSDoc(ops, function(doc) {
var pre = doc.querySelector("#test");
Expand Down
23 changes: 12 additions & 11 deletions tests/spec/core/markdown-spec.js
Expand Up @@ -205,12 +205,12 @@ describe("Core - Markdown", function() {
it("automatically links URLs in pre when missing (smoke test)", function(done) {
var ops = {
config: makeBasicConfig(),
body: makeDefaultBody() +
"<pre id=testElem>\n" +
"\t this won't link \n" +
"\t this will link: http://no-links-foo.com \n" +
"\t so will this: http://no-links-bar.com \n" +
"<pre>\n\n\n"
body: makeDefaultBody() + `
<div id=testElem>
this won't link
this will link: http://no-links-foo.com
so will this: http://no-links-bar.com
</div>`
};
ops.config.format = "markdown";
makeRSDoc(ops, function(doc) {
Expand All @@ -224,11 +224,12 @@ describe("Core - Markdown", function() {
it("replaces HTMLAnchors when present", function(done) {
var ops = {
config: makeBasicConfig(),
body: makeDefaultBody() +
"<pre id=testElem class=nolinks>\n" +
"\t http://no-links-foo.com \n" +
"\t http://no-links-bar.com \n" +
"<pre>\n\n\n"
body: makeDefaultBody() +`
<div id=testElem class=nolinks>
http://no-links-foo.com
http://no-links-bar.com
<div>
`
};
ops.config.format = "markdown";
makeRSDoc(ops, function(doc) {
Expand Down
28 changes: 27 additions & 1 deletion worker/respec-worker.js
@@ -1 +1,27 @@
// ReSpec Worker v0
// ReSpec Worker v0.1.0
"use strict";
importScripts("https://www.w3.org/Tools/respec/respec-highlight.js");

hljs.configure({
tabReplace: " ", // 2 spaces
languages: [
"css",
"http",
"javascript",
"json",
"markdown",
"xml",
"xquery",
],
});

self.addEventListener("message", function(e) {
switch (e.data.action) {
case "highlight":
const code = e.data.code;
const langs = e.data.languages.length ? e.data.languages : undefined;
const result = self.hljs.highlightAuto(code, langs);
const data = Object.assign({}, e.data, result);
self.postMessage(data);
}
});

0 comments on commit 701d542

Please sign in to comment.