element and the content box of the parent element of
* the line highlight element (either `` or ``).
*
* This offset might not be zero for some themes where the element has a top margin. Some plugins
* (or users) might also add element above the element. Because the line highlight is aligned relative
* to the element, we have to take this into account.
*
* This offset will be 0 if the parent element of the line highlight element is the `` element.
*/
var codePreOffset = !codeElement || parentElement == codeElement ? 0 : getContentBoxTopOffset(pre, codeElement);
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 + codePreOffset + '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', String(start));
if (end > start) {
line.setAttribute('data-end', String(end));
}
line.style.top = (start - offset - 1) * lineHeight + codePreOffset + '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.parentElement;
var lines = pre && pre.getAttribute('data-line');
if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
return;
}
/*
* Cleanup for other plugins (e.g. autoloader).
*
* Sometimes blocks are highlighted multiple times. It is necessary
* to cleanup any left-over tags, because the whitespace inside of the
* tags change the content of the 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.parentElement;
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);
});
})();