You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
10 KiB
346 lines
10 KiB
(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); |
|
}); |
|
|
|
}());
|
|
|