mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FIX: Fix GitHub onebox syntax highlight (#18300)
Highlight.js 11 deprecated the feature to highlight HTML blocks while keeping the HTML structure, which broke our GitHub onebox syntax highlight. This patch adds it back by bringing the maintainers code as a plugin. See https://github.com/highlightjs/highlight.js/issues/2889
This commit is contained in:
parent
8b044cbc28
commit
09cec7d6dd
@ -0,0 +1,194 @@
|
|||||||
|
// From https://github.com/highlightjs/highlight.js/issues/2889#issue-748412174
|
||||||
|
/* eslint-disable */
|
||||||
|
var mergeHTMLPlugin = (function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var originalStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function escapeHTML(value) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* plugin itself */
|
||||||
|
|
||||||
|
/** @type {HLJSPlugin} */
|
||||||
|
const mergeHTMLPlugin = {
|
||||||
|
// preserve the original HTML token stream
|
||||||
|
"before:highlightElement": ({ el }) => {
|
||||||
|
originalStream = nodeStream(el);
|
||||||
|
},
|
||||||
|
// merge it afterwards with the highlighted token stream
|
||||||
|
"after:highlightElement": ({ el, result, text }) => {
|
||||||
|
if (!originalStream.length) return;
|
||||||
|
|
||||||
|
const resultNode = document.createElement("div");
|
||||||
|
resultNode.innerHTML = result.value;
|
||||||
|
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
|
||||||
|
el.innerHTML = result.value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Stream merging support functions */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Event
|
||||||
|
* @property {'start'|'stop'} event
|
||||||
|
* @property {number} offset
|
||||||
|
* @property {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} node
|
||||||
|
*/
|
||||||
|
function tag(node) {
|
||||||
|
return node.nodeName.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} node
|
||||||
|
*/
|
||||||
|
function nodeStream(node) {
|
||||||
|
/** @type Event[] */
|
||||||
|
const result = [];
|
||||||
|
(function _nodeStream(node, offset) {
|
||||||
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||||
|
if (child.nodeType === 3) {
|
||||||
|
offset += child.nodeValue.length;
|
||||||
|
} else if (child.nodeType === 1) {
|
||||||
|
result.push({
|
||||||
|
event: "start",
|
||||||
|
offset: offset,
|
||||||
|
node: child,
|
||||||
|
});
|
||||||
|
offset = _nodeStream(child, offset);
|
||||||
|
// Prevent void elements from having an end tag that would actually
|
||||||
|
// double them in the output. There are more void elements in HTML
|
||||||
|
// but we list only those realistically expected in code display.
|
||||||
|
if (!tag(child).match(/br|hr|img|input/)) {
|
||||||
|
result.push({
|
||||||
|
event: "stop",
|
||||||
|
offset: offset,
|
||||||
|
node: child,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return offset;
|
||||||
|
})(node, 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} original - the original stream
|
||||||
|
* @param {any} highlighted - stream of the highlighted source
|
||||||
|
* @param {string} value - the original source itself
|
||||||
|
*/
|
||||||
|
function mergeStreams(original, highlighted, value) {
|
||||||
|
let processed = 0;
|
||||||
|
let result = "";
|
||||||
|
const nodeStack = [];
|
||||||
|
|
||||||
|
function selectStream() {
|
||||||
|
if (!original.length || !highlighted.length) {
|
||||||
|
return original.length ? original : highlighted;
|
||||||
|
}
|
||||||
|
if (original[0].offset !== highlighted[0].offset) {
|
||||||
|
return original[0].offset < highlighted[0].offset
|
||||||
|
? original
|
||||||
|
: highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
To avoid starting the stream just before it should stop the order is
|
||||||
|
ensured that original always starts first and closes last:
|
||||||
|
|
||||||
|
if (event1 == 'start' && event2 == 'start')
|
||||||
|
return original;
|
||||||
|
if (event1 == 'start' && event2 == 'stop')
|
||||||
|
return highlighted;
|
||||||
|
if (event1 == 'stop' && event2 == 'start')
|
||||||
|
return original;
|
||||||
|
if (event1 == 'stop' && event2 == 'stop')
|
||||||
|
return highlighted;
|
||||||
|
|
||||||
|
... which is collapsed to:
|
||||||
|
*/
|
||||||
|
return highlighted[0].event === "start" ? original : highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} node
|
||||||
|
*/
|
||||||
|
function open(node) {
|
||||||
|
/** @param {Attr} attr */
|
||||||
|
function attributeString(attr) {
|
||||||
|
return " " + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
result +=
|
||||||
|
"<" +
|
||||||
|
tag(node) +
|
||||||
|
[].map.call(node.attributes, attributeString).join("") +
|
||||||
|
">";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} node
|
||||||
|
*/
|
||||||
|
function close(node) {
|
||||||
|
result += "</" + tag(node) + ">";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
function render(event) {
|
||||||
|
(event.event === "start" ? open : close)(event.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (original.length || highlighted.length) {
|
||||||
|
let stream = selectStream();
|
||||||
|
result += escapeHTML(value.substring(processed, stream[0].offset));
|
||||||
|
processed = stream[0].offset;
|
||||||
|
if (stream === original) {
|
||||||
|
/*
|
||||||
|
On any opening or closing tag of the original markup we first close
|
||||||
|
the entire highlighted node stack, then render the original tag along
|
||||||
|
with all the following original tags at the same offset and then
|
||||||
|
reopen all the tags on the highlighted stack.
|
||||||
|
*/
|
||||||
|
nodeStack.reverse().forEach(close);
|
||||||
|
do {
|
||||||
|
render(stream.splice(0, 1)[0]);
|
||||||
|
stream = selectStream();
|
||||||
|
} while (
|
||||||
|
stream === original &&
|
||||||
|
stream.length &&
|
||||||
|
stream[0].offset === processed
|
||||||
|
);
|
||||||
|
nodeStack.reverse().forEach(open);
|
||||||
|
} else {
|
||||||
|
if (stream[0].event === "start") {
|
||||||
|
nodeStack.push(stream[0].node);
|
||||||
|
} else {
|
||||||
|
nodeStack.pop();
|
||||||
|
}
|
||||||
|
render(stream.splice(0, 1)[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result + escapeHTML(value.substr(processed));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeHTMLPlugin;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default mergeHTMLPlugin;
|
@ -1,4 +1,5 @@
|
|||||||
import loadScript from "discourse/lib/load-script";
|
import loadScript from "discourse/lib/load-script";
|
||||||
|
import mergeHTMLPlugin from "discourse/lib/highlight-syntax-merge-html-plugin";
|
||||||
|
|
||||||
/*global hljs:true */
|
/*global hljs:true */
|
||||||
let _moreLanguages = [];
|
let _moreLanguages = [];
|
||||||
@ -26,6 +27,7 @@ export default function highlightSyntax(elem, siteSettings, session) {
|
|||||||
|
|
||||||
return loadScript(path).then(() => {
|
return loadScript(path).then(() => {
|
||||||
customHighlightJSLanguages();
|
customHighlightJSLanguages();
|
||||||
|
hljs.addPlugin(mergeHTMLPlugin);
|
||||||
|
|
||||||
codeblocks.forEach((e) => {
|
codeblocks.forEach((e) => {
|
||||||
// Large code blocks can cause crashes or slowdowns
|
// Large code blocks can cause crashes or slowdowns
|
||||||
|
Loading…
Reference in New Issue
Block a user