Commit 632ce00cd2d66cb5dd56ead82ba7f95a0ad20b27

Matt Chaffe 2019-05-07T23:21:49

Line highlight: Batching DOM read/writes to avoid reflows (#1865) This batches DOM read and write operation to avoid reflows resulting for better performance.

diff --git a/plugins/line-highlight/prism-line-highlight.js b/plugins/line-highlight/prism-line-highlight.js
index 5d919ae..bebbcbf 100644
--- a/plugins/line-highlight/prism-line-highlight.js
+++ b/plugins/line-highlight/prism-line-highlight.js
@@ -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);
 	});
 
-})();
\ No newline at end of file
+})();
diff --git a/plugins/line-highlight/prism-line-highlight.min.js b/plugins/line-highlight/prism-line-highlight.min.js
index b54a86c..adfe7f3 100644
--- a/plugins/line-highlight/prism-line-highlight.min.js
+++ b/plugins/line-highlight/prism-line-highlight.min.js
@@ -1 +1 @@
-!function(){if("undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector){var t,h=function(){if(void 0===t){var e=document.createElement("div");e.style.fontSize="13px",e.style.lineHeight="1.5",e.style.padding=0,e.style.border=0,e.innerHTML="&nbsp;<br />&nbsp;",document.body.appendChild(e),t=38===e.offsetHeight,document.body.removeChild(e)}return t},l=0;Prism.hooks.add("before-sanity-check",function(e){var t=e.element.parentNode,n=t&&t.getAttribute("data-line");if(t&&n&&/pre/i.test(t.nodeName)){var i=0;r(".line-highlight",t).forEach(function(e){i+=e.textContent.length,e.parentNode.removeChild(e)}),i&&/^( \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}}),Prism.hooks.add("complete",function e(t){var n=t.element.parentNode,i=n&&n.getAttribute("data-line");if(n&&i&&/pre/i.test(n.nodeName)){clearTimeout(l);var r=Prism.plugins.lineNumbers,o=t.plugins&&t.plugins.lineNumbers;g(n,"line-numbers")&&r&&!o?Prism.hooks.add("line-numbers",e):(a(n,i),l=setTimeout(s,1))}}),window.addEventListener("hashchange",s),window.addEventListener("resize",function(){var e=document.querySelectorAll("pre[data-line]");Array.prototype.forEach.call(e,function(e){a(e)})})}function r(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function g(e,t){return t=" "+t+" ",-1<(" "+e.className+" ").replace(/[\n\t]/g," ").indexOf(t)}function a(e,t,n){for(var i,r=(t="string"==typeof t?t:e.getAttribute("data-line")).replace(/\s+/g,"").split(","),o=+e.getAttribute("data-line-offset")||0,l=(h()?parseInt:parseFloat)(getComputedStyle(e).lineHeight),a=g(e,"line-numbers"),s=0;i=r[s++];){var d=i.split("-"),u=+d[0],c=+d[1]||u,m=e.querySelector('.line-highlight[data-range="'+i+'"]')||document.createElement("div");if(m.setAttribute("aria-hidden","true"),m.setAttribute("data-range",i),m.className=(n||"")+" line-highlight",a&&Prism.plugins.lineNumbers){var p=Prism.plugins.lineNumbers.getLine(e,u),f=Prism.plugins.lineNumbers.getLine(e,c);p&&(m.style.top=p.offsetTop+"px"),f&&(m.style.height=f.offsetTop-p.offsetTop+f.offsetHeight+"px")}else m.setAttribute("data-start",u),u<c&&m.setAttribute("data-end",c),m.style.top=(u-o-1)*l+"px",m.textContent=new Array(c-u+2).join(" \n");a?e.appendChild(m):(e.querySelector("code")||e).appendChild(m)}}function s(){var e=location.hash.slice(1);r(".temporary.line-highlight").forEach(function(e){e.parentNode.removeChild(e)});var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var n=e.slice(0,e.lastIndexOf(".")),i=document.getElementById(n);i&&(i.hasAttribute("data-line")||i.setAttribute("data-line",""),a(i,t,"temporary "),document.querySelector(".temporary.line-highlight").scrollIntoView())}}}();
\ No newline at end of file
+!function(){if("undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector){var t,n=function(){if(void 0===t){var e=document.createElement("div");e.style.fontSize="13px",e.style.lineHeight="1.5",e.style.padding=0,e.style.border=0,e.innerHTML="&nbsp;<br />&nbsp;",document.body.appendChild(e),t=38===e.offsetHeight,document.body.removeChild(e)}return t},a=0;Prism.hooks.add("before-sanity-check",function(e){var t=e.element.parentNode,n=t&&t.getAttribute("data-line");if(t&&n&&/pre/i.test(t.nodeName)){var i=0;r(".line-highlight",t).forEach(function(e){i+=e.textContent.length,e.parentNode.removeChild(e)}),i&&/^( \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}}),Prism.hooks.add("complete",function e(t){var n=t.element.parentNode,i=n&&n.getAttribute("data-line");if(n&&i&&/pre/i.test(n.nodeName)){clearTimeout(a);var r=Prism.plugins.lineNumbers,o=t.plugins&&t.plugins.lineNumbers;if(l(n,"line-numbers")&&r&&!o)Prism.hooks.add("line-numbers",e);else s(n,i)(),a=setTimeout(u,1)}}),window.addEventListener("hashchange",u),window.addEventListener("resize",function(){var t=[];r("pre[data-line]").forEach(function(e){t.push(s(e))}),t.forEach(i)})}function r(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function l(e,t){return t=" "+t+" ",-1<(" "+e.className+" ").replace(/[\n\t]/g," ").indexOf(t)}function i(e){e()}function s(u,e,d){var t=(e="string"==typeof e?e:u.getAttribute("data-line")).replace(/\s+/g,"").split(","),c=+u.getAttribute("data-line-offset")||0,f=(n()?parseInt:parseFloat)(getComputedStyle(u).lineHeight),h=l(u,"line-numbers"),p=h?u:u.querySelector("code")||u,m=[];return t.forEach(function(e){var t=e.split("-"),n=+t[0],i=+t[1]||n,r=u.querySelector('.line-highlight[data-range="'+e+'"]')||document.createElement("div");if(m.push(function(){r.setAttribute("aria-hidden","true"),r.setAttribute("data-range",e),r.className=(d||"")+" line-highlight"}),h&&Prism.plugins.lineNumbers){var o=Prism.plugins.lineNumbers.getLine(u,n),a=Prism.plugins.lineNumbers.getLine(u,i);if(o){var l=o.offsetTop+"px";m.push(function(){r.style.top=l})}if(a){var s=a.offsetTop-o.offsetTop+a.offsetHeight+"px";m.push(function(){r.style.height=s})}}else m.push(function(){r.setAttribute("data-start",n),n<i&&r.setAttribute("data-end",i),r.style.top=(n-c-1)*f+"px",r.textContent=new Array(i-n+2).join(" \n")});m.push(function(){p.appendChild(r)})}),function(){m.forEach(i)}}function u(){var e=location.hash.slice(1);r(".temporary.line-highlight").forEach(function(e){e.parentNode.removeChild(e)});var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var n=e.slice(0,e.lastIndexOf(".")),i=document.getElementById(n);if(i)i.hasAttribute("data-line")||i.setAttribute("data-line",""),s(i,t,"temporary ")(),document.querySelector(".temporary.line-highlight").scrollIntoView()}}}();
\ No newline at end of file