(function () { if (typeof Prism === 'undefined' || typeof document === 'undefined' || !document.querySelector) { return; } var LINE_NUMBERS_CLASS = 'line-numbers'; var LINKABLE_LINE_NUMBERS_CLASS = 'linkable-line-numbers'; var NEW_LINE_EXP = /\n(?!$)/g; /** * @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) { return element.classList.contains(className); } /** * 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; }; }()); /** * Returns the top offset of the content box of the given parent and the content box of one of its children. * * @param {HTMLElement} parent * @param {HTMLElement} child */ function getContentBoxTopOffset(parent, child) { var parentStyle = getComputedStyle(parent); var childStyle = getComputedStyle(child); /** * Returns the numeric value of the given pixel value. * * @param {string} px */ function pxToNumber(px) { return +px.substr(0, px.length - 2); } return child.offsetTop + pxToNumber(childStyle.borderTopWidth) + pxToNumber(childStyle.paddingTop) - pxToNumber(parentStyle.paddingTop); } /** * Returns whether the Line Highlight plugin is active for the given element. * * If this function returns `false`, do not call `highlightLines` for the given element. * * @param {HTMLElement | null | undefined} pre * @returns {boolean} */ function isActiveFor(pre) { if (!pre || !/pre/i.test(pre.nodeName)) { return false; } if (pre.hasAttribute('data-line')) { return true; } if (pre.id && Prism.util.isActive(pre, LINKABLE_LINE_NUMBERS_CLASS)) { // Technically, the line numbers plugin is also necessary but this plugin doesn't control the classes of // the line numbers plugin, so we can't assume that they are present. return true; } return false; } var scrollIntoView = true; Prism.plugins.lineHighlight = { /** * 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 | null} [lines] * @param {string} [classes=''] * @returns {() => void} */ highlightLines: 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 = Prism.util.isActive(pre, LINE_NUMBERS_CLASS); var codeElement = pre.querySelector('code'); var parentElement = hasLineNumbers ? pre : codeElement || pre; var mutateActions = /** @type {(() => void)[]} */ ([]); var lineBreakMatch = codeElement.textContent.match(NEW_LINE_EXP); var numberOfLines = lineBreakMatch ? lineBreakMatch.length + 1 : 1; /** * The top offset between the content box of the <code> element and the content box of the parent element of * the line highlight element (either `<pre>` or `<code>`). * * This offset might not be zero for some themes where the <code> element has a top margin. Some plugins * (or users) might also add element above the <code> element. Because the line highlight is aligned relative * to the <pre> element, we have to take this into account. * * This offset will be 0 if the parent element of the line highlight element is the `<code>` 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; end = Math.min(numberOfLines + offset, end); if (end < start) { return; } /** @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 () { line.style.width = pre.scrollWidth + 'px'; }); 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 && Prism.util.isActive(pre, LINKABLE_LINE_NUMBERS_CLASS) && 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. if (!hasClass(pre, LINKABLE_LINE_NUMBERS_CLASS)) { // add class to pre mutateActions.push(function () { pre.classList.add(LINKABLE_LINE_NUMBERS_CLASS); }); } 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); }; } }; 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('.')); var pre = document.getElementById(id); if (!pre) { return; } if (!pre.hasAttribute('data-line')) { pre.setAttribute('data-line', ''); } var mutateDom = Prism.plugins.lineHighlight.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; if (!isActiveFor(pre)) { 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.parentElement; if (!isActiveFor(pre)) { return; } clearTimeout(fakeTimer); var hasLineNumbers = Prism.plugins.lineNumbers; var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers; if (hasClass(pre, LINE_NUMBERS_CLASS) && hasLineNumbers && !isLineNumbersLoaded) { Prism.hooks.add('line-numbers', completeHook); } else { var mutateDom = Prism.plugins.lineHighlight.highlightLines(pre); mutateDom(); fakeTimer = setTimeout(applyHash, 1); } }); window.addEventListener('hashchange', applyHash); window.addEventListener('resize', function () { var actions = $$('pre') .filter(isActiveFor) .map(function (pre) { return Prism.plugins.lineHighlight.highlightLines(pre); }); actions.forEach(callFunction); }); }());