Skip to content

Commit

Permalink
Fix asciidoctor callouts with highlight.js 11.x
Browse files Browse the repository at this point in the history
  • Loading branch information
bric3 committed Oct 17, 2022
1 parent 01f0270 commit 2d7aab5
Showing 1 changed file with 190 additions and 0 deletions.
190 changes: 190 additions & 0 deletions layouts/partials/highlight-js.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,196 @@
{{ end }}
{{ end }}
<script type="text/javascript">
// highlight.js 11 removed the 'HTML auto-merging' internal plugin
// however this is required for AsciiDoc to insert callouts
// fortunately the issue description shows how to re-enable it, by registering the plugin
// Copy-pasted code from https://github.com/highlightjs/highlight.js/issues/2889
// See also
var mergeHTMLPlugin = (function () {
'use strict';

var originalStream;

/**
* @param {string} value
* @returns {string}
*/
function escapeHTML(value) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}

/* 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;

}());
hljs.addPlugin(mergeHTMLPlugin);
// end of copy-pasted code


{{ with $.Scratch.Get "hl_languages" }}
hljs.configure({languages: [{{(delimit . ", ")}}]});
{{ end }}
Expand Down

0 comments on commit 2d7aab5

Please sign in to comment.