diff --git a/app/assets/javascripts/admin/components/highlighted-code.js b/app/assets/javascripts/admin/components/highlighted-code.js index 30f89e40850..9159bb574ad 100644 --- a/app/assets/javascripts/admin/components/highlighted-code.js +++ b/app/assets/javascripts/admin/components/highlighted-code.js @@ -1,37 +1,11 @@ import Component from "@ember/component"; -import { highlightText } from "discourse/lib/highlight-syntax"; -import { escapeExpression } from "discourse/lib/utilities"; -import discourseComputed from "discourse-common/utils/decorators"; -import { htmlSafe } from "@ember/template"; +import { on, observes } from "discourse-common/utils/decorators"; +import highlightSyntax from "discourse/lib/highlight-syntax"; export default Component.extend({ - didReceiveAttrs() { - this._super(...arguments); - if (this.code === this.previousCode) return; - - 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(" "); + @on("didInsertElement") + @observes("code") + _refresh: function() { + highlightSyntax($(this.element)); } }); diff --git a/app/assets/javascripts/admin/models/theme.js b/app/assets/javascripts/admin/models/theme.js index d17b87dfd0d..0b96660f99b 100644 --- a/app/assets/javascripts/admin/models/theme.js +++ b/app/assets/javascripts/admin/models/theme.js @@ -7,8 +7,8 @@ import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { ajax } from "discourse/lib/ajax"; import { escapeExpression } from "discourse/lib/utilities"; -import { url } from "discourse/lib/computed"; import highlightSyntax from "discourse/lib/highlight-syntax"; +import { url } from "discourse/lib/computed"; const THEME_UPLOAD_VAR = 2; const FIELDS_IDS = [0, 1, 5]; @@ -321,7 +321,7 @@ const Theme = RestModel.extend({ } } ); - highlightSyntax(document.querySelector(".bootbox.modal")); + highlightSyntax(); } else { return this.save({ remote_update: true }).then(() => this.set("changed", false) diff --git a/app/assets/javascripts/admin/templates/components/highlighted-code.hbs b/app/assets/javascripts/admin/templates/components/highlighted-code.hbs index 5dd40c0476b..4d67dd6fd2e 100644 --- a/app/assets/javascripts/admin/templates/components/highlighted-code.hbs +++ b/app/assets/javascripts/admin/templates/components/highlighted-code.hbs @@ -1 +1 @@ -
{{displayCode}}
+
{{code}}
diff --git a/app/assets/javascripts/discourse/app/initializers/post-decorations.js b/app/assets/javascripts/discourse/app/initializers/post-decorations.js index d1205bcfdb1..7b7d543aa87 100644 --- a/app/assets/javascripts/discourse/app/initializers/post-decorations.js +++ b/app/assets/javascripts/discourse/app/initializers/post-decorations.js @@ -1,15 +1,15 @@ +import highlightSyntax from "discourse/lib/highlight-syntax"; import lightbox from "discourse/lib/lightbox"; import { setupLazyLoading } from "discourse/lib/lazy-load-images"; import { setTextDirections } from "discourse/lib/text-direction"; import { withPluginApi } from "discourse/lib/plugin-api"; -import highlightSyntax from "discourse/lib/highlight-syntax"; export default { name: "post-decorations", initialize(container) { withPluginApi("0.1", api => { const siteSettings = container.lookup("site-settings:main"); - api.decorateCookedElement(highlightSyntax, { + api.decorateCooked(highlightSyntax, { id: "discourse-syntax-highlighting" }); api.decorateCookedElement(lightbox, { id: "discourse-lightbox" }); diff --git a/app/assets/javascripts/discourse/app/lib/highlight-syntax.js b/app/assets/javascripts/discourse/app/lib/highlight-syntax.js index 302fd4e62a0..404e73cadcc 100644 --- a/app/assets/javascripts/discourse/app/lib/highlight-syntax.js +++ b/app/assets/javascripts/discourse/app/lib/highlight-syntax.js @@ -1,181 +1,40 @@ -import { Promise } from "rsvp"; -import { getURLWithCDN } from "discourse-common/lib/get-url"; -import { next, schedule } from "@ember/runloop"; +/*global hljs:true */ +let _moreLanguages = []; + import loadScript from "discourse/lib/load-script"; -import { isTesting } from "discourse-common/config/environment"; -let highlightJsUrl; -let highlightJsWorkerUrl; +export default function highlightSyntax($elem) { + const selector = Discourse.SiteSettings.autohighlight_all_code + ? "pre code" + : "pre code[class]", + path = Discourse.HighlightJSPath; -const _moreLanguages = []; -let _worker = null; -let _workerPromise = null; -const _pendingResolution = {}; -let _counter = 0; -let _cachedResultsMap = new Map(); + if (!path) { + return; + } -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) { - highlightJsUrl = args.highlightJsUrl; - highlightJsWorkerUrl = args.highlightJsWorkerUrl; + $(e).removeClass("lang-auto"); + loadScript(path).then(() => { + customHighlightJSLanguages(); + hljs.highlightBlock(e); + }); + }); } export function registerHighlightJSLanguage(name, fn) { _moreLanguages.push({ name: name, fn: fn }); } -export default function highlightSyntax(elem, { autoHighlight = false } = {}) { - const selector = autoHighlight ? "pre code" : "pre code[class]"; - - elem.querySelectorAll(selector).forEach(e => highlightElement(e)); -} - -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)); +function customHighlightJSLanguages() { + _moreLanguages.forEach(l => { + if (hljs.getLanguage(l.name) === undefined) { + hljs.registerLanguage(l.name, l.fn); } }); } - -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)); - }); -} diff --git a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js index 54f55d897f3..d8eb01dadc6 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js @@ -9,7 +9,6 @@ import { } from "discourse-common/config/environment"; import { setupURL, setupS3CDN } from "discourse-common/lib/get-url"; import deprecated from "discourse-common/lib/deprecated"; -import { setupHighlightJs } from "discourse/lib/highlight-syntax"; export default { name: "discourse-bootstrap", @@ -82,11 +81,7 @@ export default { Session.currentProp("safe_mode", setupData.safeMode); } - setupHighlightJs({ - highlightJsUrl: setupData.highlightJsUrl, - highlightJsWorkerUrl: setupData.highlightJsWorkerUrl - }); - + app.HighlightJSPath = setupData.highlightJsPath; app.SvgSpritePath = setupData.svgSpritePath; if (app.Environment === "development") { diff --git a/app/assets/javascripts/highlightjs-worker.js b/app/assets/javascripts/highlightjs-worker.js deleted file mode 100644 index 64dfefbf2ec..00000000000 --- a/app/assets/javascripts/highlightjs-worker.js +++ /dev/null @@ -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}`; - } -}; diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c051ec5b316..f6bda658a38 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -469,8 +469,7 @@ module ApplicationHelper default_locale: SiteSetting.default_locale, asset_version: Discourse.assets_digest, disable_custom_css: loading_admin?, - highlight_js_url: HighlightJs.path, - highlight_js_worker_url: script_asset_path('highlightjs-worker'), + highlight_js_path: HighlightJs.path, svg_sprite_path: SvgSprite.path(theme_ids), enable_js_error_reporting: GlobalSetting.enable_js_error_reporting, } diff --git a/config/application.rb b/config/application.rb index 194cc262350..6120831f6dd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -171,7 +171,6 @@ module Discourse confirm-new-email/bootstrap.js onpopstate-handler.js embed-application.js - highlightjs-worker.js } # Precompile all available locales diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb index e0ee89e0efd..274fda206d9 100644 --- a/lib/discourse_js_processor.rb +++ b/lib/discourse_js_processor.rb @@ -56,7 +56,6 @@ class DiscourseJsProcessor activate-account auto-redirect embed-application - highlightjs-worker app-boot ).any? { |f| relative_path == "#{js_root}/#{f}.js" } diff --git a/test/javascripts/components/highlighted-code-test.js b/test/javascripts/components/highlighted-code-test.js index 1cde451bcab..27cfe5696d3 100644 --- a/test/javascripts/components/highlighted-code-test.js +++ b/test/javascripts/components/highlighted-code-test.js @@ -1,8 +1,4 @@ import componentTest from "helpers/component-test"; -import { - waitForHighlighting, - setupHighlightJs -} from "discourse/lib/highlight-syntax"; const LONG_CODE_BLOCK = "puts a\n".repeat(15000); @@ -12,15 +8,12 @@ componentTest("highlighting code", { template: "{{highlighted-code lang='ruby' code=code}}", beforeEach() { - setupHighlightJs({ - highlightJsUrl: "/assets/highlightjs/highlight-test-bundle.min.js", - highlightJsWorkerUrl: "/assets/highlightjs-worker.js" - }); + Discourse.HighlightJSPath = + "assets/highlightjs/highlight-test-bundle.min.js"; + this.set("code", "def test; end"); }, async test(assert) { - this.set("code", "def test; end"); - await waitForHighlighting(); assert.equal( find("code.ruby.hljs .hljs-function .hljs-keyword") .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}}", beforeEach() { - setupHighlightJs({ - highlightJsUrl: "/assets/highlightjs/highlight-test-bundle.min.js", - highlightJsWorkerUrl: "/assets/highlightjs-worker.js" - }); + Discourse.HighlightJSPath = + "assets/highlightjs/highlight-test-bundle.min.js"; + this.set("code", LONG_CODE_BLOCK); }, async test(assert) { - this.set("code", LONG_CODE_BLOCK); - await waitForHighlighting(); assert.equal( find("code") .text()