Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: do syntax highlighting in web worker (closes #964) #1060

Merged
merged 8 commits into from Feb 1, 2017
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);
}
});