281 lines
7.8 KiB
JavaScript
281 lines
7.8 KiB
JavaScript
(function () {
|
|
|
|
if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {ParentNode} [container]
|
|
* @returns {HTMLElement[]}
|
|
*/
|
|
function $$(selector, container) {
|
|
return Array.prototype.slice.call((container || document).querySelectorAll(selector));
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given element has the given class.
|
|
*
|
|
* @param {Element} element
|
|
* @param {string} className
|
|
* @returns {boolean}
|
|
*/
|
|
function hasClass(element, className) {
|
|
className = " " + className + " ";
|
|
return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
|
|
}
|
|
|
|
/**
|
|
* Calls the given function.
|
|
*
|
|
* @param {() => any} func
|
|
* @returns {void}
|
|
*/
|
|
function callFunction(func) {
|
|
func();
|
|
}
|
|
|
|
// Some browsers round the line-height, others don't.
|
|
// We need to test for it to position the elements properly.
|
|
var isLineHeightRounded = (function () {
|
|
var res;
|
|
return function () {
|
|
if (typeof res === 'undefined') {
|
|
var d = document.createElement('div');
|
|
d.style.fontSize = '13px';
|
|
d.style.lineHeight = '1.5';
|
|
d.style.padding = '0';
|
|
d.style.border = '0';
|
|
d.innerHTML = ' <br /> ';
|
|
document.body.appendChild(d);
|
|
// Browsers that round the line-height should have offsetHeight === 38
|
|
// The others should have 39.
|
|
res = d.offsetHeight === 38;
|
|
document.body.removeChild(d);
|
|
}
|
|
return res;
|
|
}
|
|
}());
|
|
|
|
/**
|
|
* Highlights the lines of the given pre.
|
|
*
|
|
* This function is split into a DOM measuring and mutate phase to improve performance.
|
|
* The returned function mutates the DOM when called.
|
|
*
|
|
* @param {HTMLElement} pre
|
|
* @param {string} [lines]
|
|
* @param {string} [classes='']
|
|
* @returns {() => void}
|
|
*/
|
|
function highlightLines(pre, lines, classes) {
|
|
lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line');
|
|
|
|
var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean);
|
|
var offset = +pre.getAttribute('data-line-offset') || 0;
|
|
|
|
var parseMethod = isLineHeightRounded() ? parseInt : parseFloat;
|
|
var lineHeight = parseMethod(getComputedStyle(pre).lineHeight);
|
|
var hasLineNumbers = hasClass(pre, 'line-numbers');
|
|
var parentElement = hasLineNumbers ? pre : pre.querySelector('code') || pre;
|
|
var mutateActions = /** @type {(() => void)[]} */ ([]);
|
|
|
|
ranges.forEach(function (currentRange) {
|
|
var range = currentRange.split('-');
|
|
|
|
var start = +range[0];
|
|
var end = +range[1] || start;
|
|
|
|
/** @type {HTMLElement} */
|
|
var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div');
|
|
|
|
mutateActions.push(function () {
|
|
line.setAttribute('aria-hidden', 'true');
|
|
line.setAttribute('data-range', currentRange);
|
|
line.className = (classes || '') + ' line-highlight';
|
|
});
|
|
|
|
// if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers
|
|
if (hasLineNumbers && Prism.plugins.lineNumbers) {
|
|
var startNode = Prism.plugins.lineNumbers.getLine(pre, start);
|
|
var endNode = Prism.plugins.lineNumbers.getLine(pre, end);
|
|
|
|
if (startNode) {
|
|
var top = startNode.offsetTop + 'px';
|
|
mutateActions.push(function () {
|
|
line.style.top = top;
|
|
});
|
|
}
|
|
|
|
if (endNode) {
|
|
var height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px';
|
|
mutateActions.push(function () {
|
|
line.style.height = height;
|
|
});
|
|
}
|
|
} else {
|
|
mutateActions.push(function () {
|
|
line.setAttribute('data-start', start);
|
|
|
|
if (end > start) {
|
|
line.setAttribute('data-end', end);
|
|
}
|
|
|
|
line.style.top = (start - offset - 1) * lineHeight + 'px';
|
|
|
|
line.textContent = new Array(end - start + 2).join(' \n');
|
|
});
|
|
}
|
|
|
|
mutateActions.push(function () {
|
|
// allow this to play nicely with the line-numbers plugin
|
|
// need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning
|
|
parentElement.appendChild(line);
|
|
});
|
|
});
|
|
|
|
var id = pre.id;
|
|
if (hasLineNumbers && id) {
|
|
// This implements linkable line numbers. Linkable line numbers use Line Highlight to create a link to a
|
|
// specific line. For this to work, the pre element has to:
|
|
// 1) have line numbers,
|
|
// 2) have the `linkable-line-numbers` class or an ascendant that has that class, and
|
|
// 3) have an id.
|
|
|
|
var linkableLineNumbersClass = 'linkable-line-numbers';
|
|
var linkableLineNumbers = false;
|
|
var node = pre;
|
|
while (node) {
|
|
if (hasClass(node, linkableLineNumbersClass)) {
|
|
linkableLineNumbers = true;
|
|
break;
|
|
}
|
|
node = node.parentElement;
|
|
}
|
|
|
|
if (linkableLineNumbers) {
|
|
if (!hasClass(pre, linkableLineNumbersClass)) {
|
|
// add class to pre
|
|
mutateActions.push(function () {
|
|
pre.className = (pre.className + ' ' + linkableLineNumbersClass).trim();
|
|
});
|
|
}
|
|
|
|
var start = parseInt(pre.getAttribute('data-start') || '1');
|
|
|
|
// iterate all line number spans
|
|
$$('.line-numbers-rows > span', pre).forEach(function (lineSpan, i) {
|
|
var lineNumber = i + start;
|
|
lineSpan.onclick = function () {
|
|
var hash = id + '.' + lineNumber;
|
|
|
|
// this will prevent scrolling since the span is obviously in view
|
|
scrollIntoView = false;
|
|
location.hash = hash;
|
|
setTimeout(function () {
|
|
scrollIntoView = true;
|
|
}, 1);
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
return function () {
|
|
mutateActions.forEach(callFunction);
|
|
};
|
|
}
|
|
|
|
var scrollIntoView = true;
|
|
function applyHash() {
|
|
var hash = location.hash.slice(1);
|
|
|
|
// Remove pre-existing temporary lines
|
|
$$('.temporary.line-highlight').forEach(function (line) {
|
|
line.parentNode.removeChild(line);
|
|
});
|
|
|
|
var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1];
|
|
|
|
if (!range || document.getElementById(hash)) {
|
|
return;
|
|
}
|
|
|
|
var id = hash.slice(0, hash.lastIndexOf('.')),
|
|
pre = document.getElementById(id);
|
|
|
|
if (!pre) {
|
|
return;
|
|
}
|
|
|
|
if (!pre.hasAttribute('data-line')) {
|
|
pre.setAttribute('data-line', '');
|
|
}
|
|
|
|
var mutateDom = highlightLines(pre, range, 'temporary ');
|
|
mutateDom();
|
|
|
|
if (scrollIntoView) {
|
|
document.querySelector('.temporary.line-highlight').scrollIntoView();
|
|
}
|
|
}
|
|
|
|
var fakeTimer = 0; // Hack to limit the number of times applyHash() runs
|
|
|
|
Prism.hooks.add('before-sanity-check', function (env) {
|
|
var pre = env.element.parentNode;
|
|
var lines = pre && pre.getAttribute('data-line');
|
|
|
|
if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Cleanup for other plugins (e.g. autoloader).
|
|
*
|
|
* Sometimes <code> blocks are highlighted multiple times. It is necessary
|
|
* to cleanup any left-over tags, because the whitespace inside of the <div>
|
|
* tags change the content of the <code> tag.
|
|
*/
|
|
var num = 0;
|
|
$$('.line-highlight', pre).forEach(function (line) {
|
|
num += line.textContent.length;
|
|
line.parentNode.removeChild(line);
|
|
});
|
|
// Remove extra whitespace
|
|
if (num && /^( \n)+$/.test(env.code.slice(-num))) {
|
|
env.code = env.code.slice(0, -num);
|
|
}
|
|
});
|
|
|
|
Prism.hooks.add('complete', function completeHook(env) {
|
|
var pre = env.element.parentNode;
|
|
var lines = pre && pre.getAttribute('data-line');
|
|
|
|
if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
|
|
return;
|
|
}
|
|
|
|
clearTimeout(fakeTimer);
|
|
|
|
var hasLineNumbers = Prism.plugins.lineNumbers;
|
|
var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers;
|
|
|
|
if (hasClass(pre, 'line-numbers') && hasLineNumbers && !isLineNumbersLoaded) {
|
|
Prism.hooks.add('line-numbers', completeHook);
|
|
} else {
|
|
var mutateDom = highlightLines(pre, lines);
|
|
mutateDom();
|
|
fakeTimer = setTimeout(applyHash, 1);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('hashchange', applyHash);
|
|
window.addEventListener('resize', function () {
|
|
var actions = $$('pre[data-line]').map(function (pre) {
|
|
return highlightLines(pre);
|
|
});
|
|
actions.forEach(callFunction);
|
|
});
|
|
|
|
})();
|