Skip to content

Commit

Permalink
Line highlight: Batching DOM read/writes to avoid reflows (#1865)
Browse files Browse the repository at this point in the history
This batches DOM read and write operation to avoid reflows resulting for better performance.
  • Loading branch information
Mattchewone authored and RunDevelopment committed May 7, 2019
1 parent 4faa331 commit 632ce00
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 147 deletions.
324 changes: 178 additions & 146 deletions plugins/line-highlight/prism-line-highlight.js
Original file line number Diff line number Diff line change
@@ -1,181 +1,213 @@
(function(){

if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
return;
}

function $$(expr, con) {
return Array.prototype.slice.call((con || document).querySelectorAll(expr));
}

function hasClass(element, className) {
className = " " + className + " ";
return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
}

// 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 = '&nbsp;<br />&nbsp;';
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;
}
}());

function highlightLines(pre, lines, classes) {
lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line');

var ranges = lines.replace(/\s+/g, '').split(','),
offset = +pre.getAttribute('data-line-offset') || 0;
(function () {

var parseMethod = isLineHeightRounded() ? parseInt : parseFloat;
var lineHeight = parseMethod(getComputedStyle(pre).lineHeight);
var hasLineNumbers = hasClass(pre, 'line-numbers');

for (var i=0, currentRange; currentRange = ranges[i++];) {
var range = currentRange.split('-');
if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
return;
}

var start = +range[0],
end = +range[1] || start;
function $$(expr, con) {
return Array.prototype.slice.call((con || document).querySelectorAll(expr));
}

var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div');
function hasClass(element, className) {
className = " " + className + " ";
return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
}

line.setAttribute('aria-hidden', 'true');
line.setAttribute('data-range', currentRange);
line.className = (classes || '') + ' line-highlight';
function callFunction(func) {
func();
}

//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) {
line.style.top = startNode.offsetTop + 'px';
// 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 = '&nbsp;<br />&nbsp;';
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);
}

if (endNode) {
line.style.height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px';
}
} else {
line.setAttribute('data-start', start);
return res;
}
}());

if(end > start) {
line.setAttribute('data-end', end);
/**
* 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(',');
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;

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');
});
}

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);
});
});

//allow this to play nicely with the line-numbers plugin
if(hasLineNumbers) {
//need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning
pre.appendChild(line);
} else {
(pre.querySelector('code') || pre).appendChild(line);
}
return function () {
mutateActions.forEach(callFunction);
};
}
}

function applyHash() {
var hash = location.hash.slice(1);
function applyHash() {
var hash = location.hash.slice(1);

// Remove pre-existing temporary lines
$$('.temporary.line-highlight').forEach(function (line) {
line.parentNode.removeChild(line);
});
// Remove pre-existing temporary lines
$$('.temporary.line-highlight').forEach(function (line) {
line.parentNode.removeChild(line);
});

var range = (hash.match(/\.([\d,-]+)$/) || [,''])[1];
var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1];

if (!range || document.getElementById(hash)) {
return;
}
if (!range || document.getElementById(hash)) {
return;
}

var id = hash.slice(0, hash.lastIndexOf('.')),
pre = document.getElementById(id);
var id = hash.slice(0, hash.lastIndexOf('.')),
pre = document.getElementById(id);

if (!pre) {
return;
}
if (!pre) {
return;
}

if (!pre.hasAttribute('data-line')) {
pre.setAttribute('data-line', '');
}
if (!pre.hasAttribute('data-line')) {
pre.setAttribute('data-line', '');
}

highlightLines(pre, range, 'temporary ');
var mutateDom = highlightLines(pre, range, 'temporary ');
mutateDom();

document.querySelector('.temporary.line-highlight').scrollIntoView();
}
document.querySelector('.temporary.line-highlight').scrollIntoView();
}

var fakeTimer = 0; // Hack to limit the number of times applyHash() runs
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');
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);
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);
}
});
// 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');
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;
}
if (!pre || !lines || !/pre/i.test(pre.nodeName)) {
return;
}

clearTimeout(fakeTimer);
clearTimeout(fakeTimer);

var hasLineNumbers = Prism.plugins.lineNumbers;
var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers;
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 {
highlightLines(pre, lines);
fakeTimer = setTimeout(applyHash, 1);
}
});
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 preElements = document.querySelectorAll('pre[data-line]');
Array.prototype.forEach.call(preElements, function (pre) {
highlightLines(pre);
var actions = [];
$$('pre[data-line]').forEach(function (pre) {
actions.push(highlightLines(pre));
});
actions.forEach(callFunction);
});

})();
})();
2 changes: 1 addition & 1 deletion plugins/line-highlight/prism-line-highlight.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 632ce00

Please sign in to comment.