Skip to content

Commit d6d7f5b

Browse files
committed
Add mergeHTMLPlugin plugin for hljs
1 parent 0940a22 commit d6d7f5b

File tree

4 files changed

+207
-7
lines changed

4 files changed

+207
-7
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"bootstrap-multiselect": "1.1.0",
2727
"bootstrap-sass": "^3.3.6",
2828
"font-awesome": "^4.7.0",
29-
"highlight.js": "^11.7.0",
29+
"highlight.js": "^11.10.0",
3030
"isotope-layout": "^3.0.0",
3131
"jquery": "~3.6.3",
3232
"jquery-bridget": "^3.0.1"

src/lib/highlightjs-merge-html.js

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// From https://github.com/highlightjs/highlight.js/issues/2889#issue-748412174
2+
// Copyright (c) 2006, Ivan Sagalaev. BSD-3-Clause License
3+
4+
var mergeHTMLPlugin = (function () {
5+
6+
var originalStream;
7+
8+
/**
9+
* @param {string} value
10+
* @returns {string}
11+
*/
12+
function escapeHTML(value) {
13+
return value
14+
.replace(/&/g, '&')
15+
.replace(/</g, '&lt;')
16+
.replace(/>/g, '&gt;')
17+
.replace(/'/g, '&quot;')
18+
.replace(/'/g, '&#x27;');
19+
}
20+
21+
/* plugin itself */
22+
23+
/** @type {HLJSPlugin} */
24+
const mergeHTMLPlugin = {
25+
// preserve the original HTML token stream
26+
'before:highlightElement': ({ el }) => {
27+
originalStream = nodeStream(el);
28+
},
29+
// merge it afterwards with the highlighted token stream
30+
'after:highlightElement': ({ el, result, text }) => {
31+
if (!originalStream.length) return;
32+
33+
const resultNode = document.createElement('div');
34+
resultNode.innerHTML = result.value;
35+
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
36+
el.innerHTML = result.value;
37+
},
38+
};
39+
40+
/* Stream merging support functions */
41+
42+
/**
43+
* @typedef Event
44+
* @property {'start'|'stop'} event
45+
* @property {number} offset
46+
* @property {Node} node
47+
*/
48+
49+
/**
50+
* @param {Node} node
51+
*/
52+
function tag(node) {
53+
return node.nodeName.toLowerCase();
54+
}
55+
56+
/**
57+
* @param {Node} node
58+
*/
59+
function nodeStream(node) {
60+
/** @type Event[] */
61+
const result = [];
62+
(function _nodeStream(node, offset) {
63+
for (let child = node.firstChild; child; child = child.nextSibling) {
64+
if (child.nodeType === 3) {
65+
offset += child.nodeValue.length;
66+
} else if (child.nodeType === 1) {
67+
result.push({
68+
event: 'start',
69+
offset: offset,
70+
node: child,
71+
});
72+
offset = _nodeStream(child, offset);
73+
// Prevent void elements from having an end tag that would actually
74+
// double them in the output. There are more void elements in HTML
75+
// but we list only those realistically expected in code display.
76+
if (!tag(child).match(/br|hr|img|input/)) {
77+
result.push({
78+
event: 'stop',
79+
offset: offset,
80+
node: child,
81+
});
82+
}
83+
}
84+
}
85+
return offset;
86+
})(node, 0);
87+
return result;
88+
}
89+
90+
/**
91+
* @param {any} original - the original stream
92+
* @param {any} highlighted - stream of the highlighted source
93+
* @param {string} value - the original source itself
94+
*/
95+
function mergeStreams(original, highlighted, value) {
96+
let processed = 0;
97+
let result = '';
98+
const nodeStack = [];
99+
100+
function selectStream() {
101+
if (!original.length || !highlighted.length) {
102+
return original.length ? original : highlighted;
103+
}
104+
if (original[0].offset !== highlighted[0].offset) {
105+
return original[0].offset < highlighted[0].offset
106+
? original
107+
: highlighted;
108+
}
109+
110+
/*
111+
To avoid starting the stream just before it should stop the order is
112+
ensured that original always starts first and closes last:
113+
114+
if (event1 == 'start' && event2 == 'start')
115+
return original;
116+
if (event1 == 'start' && event2 == 'stop')
117+
return highlighted;
118+
if (event1 == 'stop' && event2 == 'start')
119+
return original;
120+
if (event1 == 'stop' && event2 == 'stop')
121+
return highlighted;
122+
123+
... which is collapsed to:
124+
*/
125+
return highlighted[0].event === 'start' ? original : highlighted;
126+
}
127+
128+
/**
129+
* @param {Node} node
130+
*/
131+
function open(node) {
132+
/** @param {Attr} attr */
133+
function attributeString(attr) {
134+
return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
135+
}
136+
// @ts-ignore
137+
result +=
138+
'<' +
139+
tag(node) +
140+
[].map.call(node.attributes, attributeString).join('') +
141+
'>';
142+
}
143+
144+
/**
145+
* @param {Node} node
146+
*/
147+
function close(node) {
148+
result += '</' + tag(node) + '>';
149+
}
150+
151+
/**
152+
* @param {Event} event
153+
*/
154+
function render(event) {
155+
(event.event === 'start' ? open : close)(event.node);
156+
}
157+
158+
while (original.length || highlighted.length) {
159+
let stream = selectStream();
160+
result += escapeHTML(value.substring(processed, stream[0].offset));
161+
processed = stream[0].offset;
162+
if (stream === original) {
163+
/*
164+
On any opening or closing tag of the original markup we first close
165+
the entire highlighted node stack, then render the original tag along
166+
with all the following original tags at the same offset and then
167+
reopen all the tags on the highlighted stack.
168+
*/
169+
nodeStack.reverse().forEach(close);
170+
do {
171+
render(stream.splice(0, 1)[0]);
172+
stream = selectStream();
173+
} while (
174+
stream === original &&
175+
stream.length &&
176+
stream[0].offset === processed
177+
);
178+
nodeStack.reverse().forEach(open);
179+
} else {
180+
if (stream[0].event === 'start') {
181+
nodeStack.push(stream[0].node);
182+
} else {
183+
nodeStack.pop();
184+
}
185+
render(stream.splice(0, 1)[0]);
186+
}
187+
}
188+
return result + escapeHTML(value.substr(processed));
189+
}
190+
191+
return mergeHTMLPlugin;
192+
})();
193+
194+
export default mergeHTMLPlugin;

src/main.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,16 @@ require('./img/warehouse-pypi-logo.png');
135135
require('./img/websauna-logo.png');
136136
require('./img/yelp-logo.png');
137137
require('./img/zopyx-logo.png');
138+
require('./lib/highlightjs-merge-html.js')
138139

139-
const hljs = require('highlight.js');
140+
import mergeHTMLPlugin from './lib/highlightjs-merge-html.js';
140141

141-
hljs.initHighlightingOnLoad();
142+
const hljs = require('highlight.js');
143+
hljs.configure({
144+
ignoreUnescapedHTML: true,
145+
});
146+
hljs.addPlugin(mergeHTMLPlugin);
147+
hljs.highlightAll();
142148

143149
if ($('.home').length){
144150
$(window).scroll(function() {

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -3396,10 +3396,10 @@ has@^1.0.3:
33963396
dependencies:
33973397
function-bind "^1.1.1"
33983398

3399-
highlight.js@^11.7.0:
3400-
version "11.7.0"
3401-
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
3402-
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
3399+
highlight.js@^11.10.0:
3400+
version "11.10.0"
3401+
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
3402+
integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==
34033403

34043404
hosted-git-info@^2.1.4:
34053405
version "2.8.9"

0 commit comments

Comments
 (0)