Revert "PERF: Move highlightjs to a background worker, and add result cache (#10191)"
This caused a CORS error when used with S3 asset storage
This reverts commit d09f283e91
.
This commit is contained in:
parent
c802c7367a
commit
7d300006a1
|
@ -1,37 +1,11 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { highlightText } from "discourse/lib/highlight-syntax";
|
import { on, observes } from "discourse-common/utils/decorators";
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
didReceiveAttrs() {
|
@on("didInsertElement")
|
||||||
this._super(...arguments);
|
@observes("code")
|
||||||
if (this.code === this.previousCode) return;
|
_refresh: function() {
|
||||||
|
highlightSyntax($(this.element));
|
||||||
this.set("previousCode", this.code);
|
|
||||||
this.set("highlightResult", null);
|
|
||||||
const toHighlight = this.code;
|
|
||||||
highlightText(escapeExpression(toHighlight), this.lang).then(
|
|
||||||
({ result }) => {
|
|
||||||
if (toHighlight !== this.code) return; // Code has changed since highlight was requested
|
|
||||||
this.set("highlightResult", result);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("code", "highlightResult")
|
|
||||||
displayCode(code, highlightResult) {
|
|
||||||
if (highlightResult) return htmlSafe(highlightResult);
|
|
||||||
return code;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("highlightResult", "lang")
|
|
||||||
codeClasses(highlightResult, lang) {
|
|
||||||
const classes = [];
|
|
||||||
if (lang) classes.push(lang);
|
|
||||||
if (highlightResult) classes.push("hljs");
|
|
||||||
|
|
||||||
return classes.join(" ");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,8 +7,8 @@ import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { url } from "discourse/lib/computed";
|
|
||||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||||
|
import { url } from "discourse/lib/computed";
|
||||||
|
|
||||||
const THEME_UPLOAD_VAR = 2;
|
const THEME_UPLOAD_VAR = 2;
|
||||||
const FIELDS_IDS = [0, 1, 5];
|
const FIELDS_IDS = [0, 1, 5];
|
||||||
|
@ -321,7 +321,7 @@ const Theme = RestModel.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
highlightSyntax(document.querySelector(".bootbox.modal"));
|
highlightSyntax();
|
||||||
} else {
|
} else {
|
||||||
return this.save({ remote_update: true }).then(() =>
|
return this.save({ remote_update: true }).then(() =>
|
||||||
this.set("changed", false)
|
this.set("changed", false)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<pre><code class={{codeClasses}}>{{displayCode}}</code></pre>
|
<pre><code class={{lang}}>{{code}}</code></pre>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||||
import lightbox from "discourse/lib/lightbox";
|
import lightbox from "discourse/lib/lightbox";
|
||||||
import { setupLazyLoading } from "discourse/lib/lazy-load-images";
|
import { setupLazyLoading } from "discourse/lib/lazy-load-images";
|
||||||
import { setTextDirections } from "discourse/lib/text-direction";
|
import { setTextDirections } from "discourse/lib/text-direction";
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "post-decorations",
|
name: "post-decorations",
|
||||||
initialize(container) {
|
initialize(container) {
|
||||||
withPluginApi("0.1", api => {
|
withPluginApi("0.1", api => {
|
||||||
const siteSettings = container.lookup("site-settings:main");
|
const siteSettings = container.lookup("site-settings:main");
|
||||||
api.decorateCookedElement(highlightSyntax, {
|
api.decorateCooked(highlightSyntax, {
|
||||||
id: "discourse-syntax-highlighting"
|
id: "discourse-syntax-highlighting"
|
||||||
});
|
});
|
||||||
api.decorateCookedElement(lightbox, { id: "discourse-lightbox" });
|
api.decorateCookedElement(lightbox, { id: "discourse-lightbox" });
|
||||||
|
|
|
@ -1,181 +1,40 @@
|
||||||
import { Promise } from "rsvp";
|
/*global hljs:true */
|
||||||
import { getURLWithCDN } from "discourse-common/lib/get-url";
|
let _moreLanguages = [];
|
||||||
import { next, schedule } from "@ember/runloop";
|
|
||||||
import loadScript from "discourse/lib/load-script";
|
import loadScript from "discourse/lib/load-script";
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
|
||||||
|
|
||||||
let highlightJsUrl;
|
export default function highlightSyntax($elem) {
|
||||||
let highlightJsWorkerUrl;
|
const selector = Discourse.SiteSettings.autohighlight_all_code
|
||||||
|
? "pre code"
|
||||||
|
: "pre code[class]",
|
||||||
|
path = Discourse.HighlightJSPath;
|
||||||
|
|
||||||
const _moreLanguages = [];
|
if (!path) {
|
||||||
let _worker = null;
|
return;
|
||||||
let _workerPromise = null;
|
}
|
||||||
const _pendingResolution = {};
|
|
||||||
let _counter = 0;
|
|
||||||
let _cachedResultsMap = new Map();
|
|
||||||
|
|
||||||
const CACHE_SIZE = 100;
|
$(selector, $elem).each(function(i, e) {
|
||||||
|
// Large code blocks can cause crashes or slowdowns
|
||||||
|
if (e.innerHTML.length > 30000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
export function setupHighlightJs(args) {
|
$(e).removeClass("lang-auto");
|
||||||
highlightJsUrl = args.highlightJsUrl;
|
loadScript(path).then(() => {
|
||||||
highlightJsWorkerUrl = args.highlightJsWorkerUrl;
|
customHighlightJSLanguages();
|
||||||
|
hljs.highlightBlock(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerHighlightJSLanguage(name, fn) {
|
export function registerHighlightJSLanguage(name, fn) {
|
||||||
_moreLanguages.push({ name: name, fn: fn });
|
_moreLanguages.push({ name: name, fn: fn });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function highlightSyntax(elem, { autoHighlight = false } = {}) {
|
function customHighlightJSLanguages() {
|
||||||
const selector = autoHighlight ? "pre code" : "pre code[class]";
|
_moreLanguages.forEach(l => {
|
||||||
|
if (hljs.getLanguage(l.name) === undefined) {
|
||||||
elem.querySelectorAll(selector).forEach(e => highlightElement(e));
|
hljs.registerLanguage(l.name, l.fn);
|
||||||
}
|
|
||||||
|
|
||||||
function highlightElement(e) {
|
|
||||||
e.classList.remove("lang-auto");
|
|
||||||
let lang = null;
|
|
||||||
e.classList.forEach(c => {
|
|
||||||
if (c.startsWith("lang-")) {
|
|
||||||
lang = c.slice("lang-".length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestString = e.textContent;
|
|
||||||
highlightText(e.textContent, lang).then(({ result, fromCache }) => {
|
|
||||||
const doRender = () => {
|
|
||||||
// Ensure the code hasn't changed since highlighting was triggered:
|
|
||||||
if (requestString !== e.textContent) return;
|
|
||||||
|
|
||||||
e.innerHTML = result;
|
|
||||||
e.classList.add("hljs");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fromCache) {
|
|
||||||
// This happened synchronously, we can safely add rendering
|
|
||||||
// to the end of the current Runloop
|
|
||||||
schedule("afterRender", null, doRender);
|
|
||||||
} else {
|
|
||||||
// This happened async, we are probably not in a runloop
|
|
||||||
// If we call `schedule`, a new runloop will be triggered immediately
|
|
||||||
// So schedule rendering to happen in the next runloop
|
|
||||||
next(() => schedule("afterRender", null, doRender));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightText(text, language) {
|
|
||||||
// Large code blocks can cause crashes or slowdowns
|
|
||||||
if (text.length > 30000) {
|
|
||||||
return Promise.resolve({ result: text, fromCache: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return getWorker().then(w => {
|
|
||||||
let result;
|
|
||||||
if ((result = _cachedResultsMap.get(cacheKey(text, language)))) {
|
|
||||||
return Promise.resolve({ result, fromCache: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolve;
|
|
||||||
const promise = new Promise(f => (resolve = f));
|
|
||||||
|
|
||||||
w.postMessage({
|
|
||||||
type: "highlight",
|
|
||||||
id: _counter,
|
|
||||||
text,
|
|
||||||
language
|
|
||||||
});
|
|
||||||
|
|
||||||
_pendingResolution[_counter] = {
|
|
||||||
promise,
|
|
||||||
resolve,
|
|
||||||
text,
|
|
||||||
language
|
|
||||||
};
|
|
||||||
|
|
||||||
_counter++;
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorker() {
|
|
||||||
if (_worker) return Promise.resolve(_worker);
|
|
||||||
if (_workerPromise) return _workerPromise;
|
|
||||||
|
|
||||||
const w = new Worker(highlightJsWorkerUrl);
|
|
||||||
w.onmessage = onWorkerMessage;
|
|
||||||
w.postMessage({
|
|
||||||
type: "loadHighlightJs",
|
|
||||||
path: fullHighlightJsUrl()
|
|
||||||
});
|
|
||||||
|
|
||||||
_workerPromise = setupCustomLanguages(w).then(() => (_worker = w));
|
|
||||||
return _workerPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupCustomLanguages(worker) {
|
|
||||||
if (_moreLanguages.length === 0) return Promise.resolve();
|
|
||||||
// To build custom language definitions we need to have hljs loaded
|
|
||||||
// Plugins/themes can't run code in a worker, so we have to load hljs in the main thread
|
|
||||||
// But the actual highlighting will still be done in the worker
|
|
||||||
|
|
||||||
return loadScript(highlightJsUrl).then(() => {
|
|
||||||
_moreLanguages.forEach(({ name, fn }) => {
|
|
||||||
const definition = fn(window.hljs);
|
|
||||||
worker.postMessage({
|
|
||||||
type: "registerLanguage",
|
|
||||||
definition,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWorkerMessage(message) {
|
|
||||||
const id = message.data.id;
|
|
||||||
const request = _pendingResolution[id];
|
|
||||||
delete _pendingResolution[id];
|
|
||||||
request.resolve({ result: message.data.result, fromCache: false });
|
|
||||||
|
|
||||||
cacheResult({
|
|
||||||
text: request.text,
|
|
||||||
language: request.language,
|
|
||||||
result: message.data.result
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function cacheResult({ text, language, result }) {
|
|
||||||
_cachedResultsMap.set(cacheKey(text, language), result);
|
|
||||||
while (_cachedResultsMap.size > CACHE_SIZE) {
|
|
||||||
_cachedResultsMap.delete(_cachedResultsMap.entries().next().value[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cacheKey(text, lang) {
|
|
||||||
return `${lang}:${text}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullHighlightJsUrl() {
|
|
||||||
let hljsUrl = getURLWithCDN(highlightJsUrl);
|
|
||||||
|
|
||||||
// Need to use full URL including protocol/domain
|
|
||||||
// for use in a worker
|
|
||||||
if (hljsUrl.startsWith("/")) {
|
|
||||||
hljsUrl = window.location.protocol + "//" + window.location.host + hljsUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hljsUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To be used in qunit tests. Running highlight in a worker means that the
|
|
||||||
// normal system which waits for ember rendering in tests doesn't work.
|
|
||||||
// This promise will resolve once all pending highlights are done
|
|
||||||
export function waitForHighlighting() {
|
|
||||||
if (!isTesting()) {
|
|
||||||
throw "This function should only be called in a test environment";
|
|
||||||
}
|
|
||||||
const promises = Object.values(_pendingResolution).map(r => r.promise);
|
|
||||||
return new Promise(resolve => {
|
|
||||||
Promise.all(promises).then(() => next(resolve));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
} from "discourse-common/config/environment";
|
} from "discourse-common/config/environment";
|
||||||
import { setupURL, setupS3CDN } from "discourse-common/lib/get-url";
|
import { setupURL, setupS3CDN } from "discourse-common/lib/get-url";
|
||||||
import deprecated from "discourse-common/lib/deprecated";
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
import { setupHighlightJs } from "discourse/lib/highlight-syntax";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "discourse-bootstrap",
|
name: "discourse-bootstrap",
|
||||||
|
@ -82,11 +81,7 @@ export default {
|
||||||
Session.currentProp("safe_mode", setupData.safeMode);
|
Session.currentProp("safe_mode", setupData.safeMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupHighlightJs({
|
app.HighlightJSPath = setupData.highlightJsPath;
|
||||||
highlightJsUrl: setupData.highlightJsUrl,
|
|
||||||
highlightJsWorkerUrl: setupData.highlightJsWorkerUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
app.SvgSpritePath = setupData.svgSpritePath;
|
app.SvgSpritePath = setupData.svgSpritePath;
|
||||||
|
|
||||||
if (app.Environment === "development") {
|
if (app.Environment === "development") {
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
// discourse-skip-module
|
|
||||||
|
|
||||||
// Standalone worker for highlightjs syntax generation
|
|
||||||
|
|
||||||
// The highlightjs path changes based on site settings,
|
|
||||||
// so we wait for Discourse to pass the path into the worker
|
|
||||||
const loadHighlightJs = path => {
|
|
||||||
self.importScripts(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlight = ({ id, text, language }) => {
|
|
||||||
if (!self.hljs) {
|
|
||||||
throw "HighlightJS is not loaded";
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = language
|
|
||||||
? self.hljs.highlight(language, text, true).value
|
|
||||||
: self.hljs.highlightAuto(text).value;
|
|
||||||
|
|
||||||
postMessage({
|
|
||||||
type: "highlightResult",
|
|
||||||
id: id,
|
|
||||||
result: result
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerLanguage = ({ name, definition }) => {
|
|
||||||
if (!self.hljs) {
|
|
||||||
throw "HighlightJS is not loaded";
|
|
||||||
}
|
|
||||||
self.hljs.registerLanguage(name, () => {
|
|
||||||
return definition;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onmessage = event => {
|
|
||||||
const data = event.data;
|
|
||||||
const messageType = data.type;
|
|
||||||
|
|
||||||
if (messageType === "loadHighlightJs") {
|
|
||||||
loadHighlightJs(data.path);
|
|
||||||
} else if (messageType === "registerLanguage") {
|
|
||||||
registerLanguage(data);
|
|
||||||
} else if (messageType === "highlight") {
|
|
||||||
highlight(data);
|
|
||||||
} else {
|
|
||||||
throw `Unknown message type: ${messageType}`;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -469,8 +469,7 @@ module ApplicationHelper
|
||||||
default_locale: SiteSetting.default_locale,
|
default_locale: SiteSetting.default_locale,
|
||||||
asset_version: Discourse.assets_digest,
|
asset_version: Discourse.assets_digest,
|
||||||
disable_custom_css: loading_admin?,
|
disable_custom_css: loading_admin?,
|
||||||
highlight_js_url: HighlightJs.path,
|
highlight_js_path: HighlightJs.path,
|
||||||
highlight_js_worker_url: script_asset_path('highlightjs-worker'),
|
|
||||||
svg_sprite_path: SvgSprite.path(theme_ids),
|
svg_sprite_path: SvgSprite.path(theme_ids),
|
||||||
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
|
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,7 +171,6 @@ module Discourse
|
||||||
confirm-new-email/bootstrap.js
|
confirm-new-email/bootstrap.js
|
||||||
onpopstate-handler.js
|
onpopstate-handler.js
|
||||||
embed-application.js
|
embed-application.js
|
||||||
highlightjs-worker.js
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Precompile all available locales
|
# Precompile all available locales
|
||||||
|
|
|
@ -56,7 +56,6 @@ class DiscourseJsProcessor
|
||||||
activate-account
|
activate-account
|
||||||
auto-redirect
|
auto-redirect
|
||||||
embed-application
|
embed-application
|
||||||
highlightjs-worker
|
|
||||||
app-boot
|
app-boot
|
||||||
).any? { |f| relative_path == "#{js_root}/#{f}.js" }
|
).any? { |f| relative_path == "#{js_root}/#{f}.js" }
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import componentTest from "helpers/component-test";
|
import componentTest from "helpers/component-test";
|
||||||
import {
|
|
||||||
waitForHighlighting,
|
|
||||||
setupHighlightJs
|
|
||||||
} from "discourse/lib/highlight-syntax";
|
|
||||||
|
|
||||||
const LONG_CODE_BLOCK = "puts a\n".repeat(15000);
|
const LONG_CODE_BLOCK = "puts a\n".repeat(15000);
|
||||||
|
|
||||||
|
@ -12,15 +8,12 @@ componentTest("highlighting code", {
|
||||||
template: "{{highlighted-code lang='ruby' code=code}}",
|
template: "{{highlighted-code lang='ruby' code=code}}",
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
setupHighlightJs({
|
Discourse.HighlightJSPath =
|
||||||
highlightJsUrl: "/assets/highlightjs/highlight-test-bundle.min.js",
|
"assets/highlightjs/highlight-test-bundle.min.js";
|
||||||
highlightJsWorkerUrl: "/assets/highlightjs-worker.js"
|
this.set("code", "def test; end");
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async test(assert) {
|
async test(assert) {
|
||||||
this.set("code", "def test; end");
|
|
||||||
await waitForHighlighting();
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find("code.ruby.hljs .hljs-function .hljs-keyword")
|
find("code.ruby.hljs .hljs-function .hljs-keyword")
|
||||||
.text()
|
.text()
|
||||||
|
@ -30,19 +23,16 @@ componentTest("highlighting code", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest("highlighting code limit", {
|
componentTest("large code blocks are not highlighted", {
|
||||||
template: "{{highlighted-code lang='ruby' code=code}}",
|
template: "{{highlighted-code lang='ruby' code=code}}",
|
||||||
|
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
setupHighlightJs({
|
Discourse.HighlightJSPath =
|
||||||
highlightJsUrl: "/assets/highlightjs/highlight-test-bundle.min.js",
|
"assets/highlightjs/highlight-test-bundle.min.js";
|
||||||
highlightJsWorkerUrl: "/assets/highlightjs-worker.js"
|
this.set("code", LONG_CODE_BLOCK);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async test(assert) {
|
async test(assert) {
|
||||||
this.set("code", LONG_CODE_BLOCK);
|
|
||||||
await waitForHighlighting();
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find("code")
|
find("code")
|
||||||
.text()
|
.text()
|
||||||
|
|
Loading…
Reference in New Issue