From 7eb2cec6b6c3353b485f46c1dbf74d535f115234 Mon Sep 17 00:00:00 2001 From: Hakim El Hattab Date: Fri, 5 Apr 2019 07:59:28 +0200 Subject: [PATCH] first version of multi-step code highlights --- css/reveal.css | 10 +++ css/reveal.scss | 12 +++ demo.html | 4 +- index.html | 4 +- plugin/highlight/highlight.js | 161 ++++++++++++++++++++++++++++++---- 5 files changed, 169 insertions(+), 22 deletions(-) diff --git a/css/reveal.css b/css/reveal.css index 1b9651b..d549340 100644 --- a/css/reveal.css +++ b/css/reveal.css @@ -1456,6 +1456,16 @@ body { .reveal .hljs[data-line-numbers]:not([data-line-numbers=""]) tr:not(.highlight-line) { opacity: 0.4; } +.reveal .hljs .highlight-line .hljs-ln-numbers { + font-weight: 600; } + +.reveal .hljs:not(:first-child).fragment { + position: absolute; + top: 0; + left: 0; + width: 100%; + box-sizing: border-box; } + /********************************************* * ROLLING LINKS *********************************************/ diff --git a/css/reveal.scss b/css/reveal.scss index ab732a4..0b7718a 100644 --- a/css/reveal.scss +++ b/css/reveal.scss @@ -1594,6 +1594,18 @@ $controlsArrowAngleActive: 36deg; opacity: 0.4; } +.reveal .hljs .highlight-line .hljs-ln-numbers { + font-weight: 600; +} + +.reveal .hljs:not(:first-child).fragment { + position: absolute; + top: 0; + left: 0; + width: 100%; + box-sizing: border-box; +} + /********************************************* * ROLLING LINKS diff --git a/demo.html b/demo.html index f88bfa2..cf05e88 100644 --- a/demo.html +++ b/demo.html @@ -241,7 +241,7 @@

Pretty Code

-

+					

 import React, { useState } from 'react';
 
 function Example() {
@@ -412,7 +412,7 @@ Reveal.addEventListener( 'customevent', function() {
 				dependencies: [
 					{ src: 'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
 					{ src: 'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
-					{ src: 'plugin/highlight/highlight.js', async: true },
+					{ src: 'plugin/highlight/highlight.js' },
 					{ src: 'plugin/search/search.js', async: true },
 					{ src: 'plugin/zoom-js/zoom.js', async: true },
 					{ src: 'plugin/notes/notes.js', async: true }
diff --git a/index.html b/index.html
index f938be5..a4825a9 100644
--- a/index.html
+++ b/index.html
@@ -40,8 +40,8 @@
 				dependencies: [
 					{ src: 'plugin/markdown/marked.js' },
 					{ src: 'plugin/markdown/markdown.js' },
-					{ src: 'plugin/notes/notes.js', async: true },
-					{ src: 'plugin/highlight/highlight.js', async: true }
+					{ src: 'plugin/highlight/highlight.js' },
+					{ src: 'plugin/notes/notes.js', async: true }
 				]
 			});
 		
diff --git a/plugin/highlight/highlight.js b/plugin/highlight/highlight.js
index 83305e1..b271261 100644
--- a/plugin/highlight/highlight.js
+++ b/plugin/highlight/highlight.js
@@ -68,6 +68,11 @@ c:[{cN:"comment",b:/\(\*/,e:/\*\)/},e.ASM,e.QSM,e.CNM,{b:/\{/,e:/\}/,i:/:/}]}});
 	}
 
 	var RevealHighlight = {
+
+		HIGHLIGHT_STEP_DELIMITER: '|',
+		HIGHLIGHT_LINE_DELIMITER: ',',
+		HIGHLIGHT_LINE_RANGE_DELIMITER: '-',
+
 		init: function() {
 
 			// Read the plugin config options and provide fallbacks
@@ -103,6 +108,10 @@ c:[{cN:"comment",b:/\(\*/,e:/\*\)/},e.ASM,e.QSM,e.CNM,{b:/\{/,e:/\}/,i:/:/}]}});
 		 * Highlights a code block. If the  node has the
 		 * 'data-line-numbers' attribute we also generate slide
 		 * numbers.
+		 *
+		 * If a code block contains multiple line highlight steps
+		 * we duplicate the code block once per lines that should
+		 * be highlighted.
 		 */
 		highlightBlock: function( block ) {
 
@@ -113,7 +122,45 @@ c:[{cN:"comment",b:/\(\*/,e:/\*\)/},e.ASM,e.QSM,e.CNM,{b:/\{/,e:/\}/,i:/:/}]}});
 
 				// hljs.lineNumbersBlock runs async code on the next cycle,
 				// so we need to do the same to execute after it's done
-				setTimeout( RevealHighlight.highlightLines.bind( this, block ), 0 );
+				setTimeout( function() {
+
+					var highlightSteps = RevealHighlight.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) );
+
+					// If there are at least two highlight steps, generate
+					// fragment clones for each
+					if( highlightSteps.length > 1 ) {
+
+						// If the original code block has a fragment-index,
+						// each clone should increment from that index
+						var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 );
+						if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) {
+							fragmentIndex = null;
+						}
+
+						// Generate fragments for all except the first step/original block
+						highlightSteps.slice(1).forEach( function( highlight ) {
+
+							var fragmentBlock = block.cloneNode( true );
+							fragmentBlock.setAttribute( 'data-line-numbers', RevealHighlight.serializeHighlightSteps( [ highlight ] ) );
+							fragmentBlock.classList.add( 'fragment' );
+							block.parentNode.appendChild( fragmentBlock );
+							RevealHighlight.highlightLines( fragmentBlock );
+
+							if( fragmentIndex ) {
+								fragmentBlock.setAttribute( 'data-fragment-index', fragmentIndex );
+								fragmentIndex += 1;
+							}
+
+						} );
+
+						block.setAttribute( 'data-line-numbers', RevealHighlight.serializeHighlightSteps( [ highlightSteps[0] ] ) );
+
+					}
+
+					RevealHighlight.highlightLines( block );
+
+				}.bind( this ), 0 );
+
 			}
 
 		},
@@ -131,34 +178,112 @@ c:[{cN:"comment",b:/\(\*/,e:/\*\)/},e.ASM,e.QSM,e.CNM,{b:/\{/,e:/\}/,i:/:/}]}});
 		 */
 		highlightLines: function( block, linesToHighlight ) {
 
-			linesToHighlight = linesToHighlight || block.getAttribute( 'data-line-numbers' );
+			var highlightSteps = RevealHighlight.deserializeHighlightSteps( linesToHighlight || block.getAttribute( 'data-line-numbers' ) );
 
-			if( typeof linesToHighlight === 'string' && linesToHighlight !== '' ) {
+			if( highlightSteps.length ) {
 
-				linesToHighlight.split( ',' ).forEach( function( lineNumbers ) {
+				highlightSteps[0].forEach( function( highlight ) {
 
-					// Avoid failures becase of whitespace
-					lineNumbers = lineNumbers.replace( /\s/g, '' );
-
-					// Ensure that we looking at a valid slide number (1 or 1-2)
-					if( /^[\d-]+$/.test( lineNumbers ) ) {
-
-						lineNumbers = lineNumbers.split( '-' );
-
-						var lineStart = lineNumbers[0];
-						var lineEnd = lineNumbers[1] || lineStart;
-
-						[].slice.call( block.querySelectorAll( 'table tr:nth-child(n+'+lineStart+'):nth-child(-n+'+lineEnd+')' ) ).forEach( function( lineElement ) {
-							lineElement.classList.add( 'highlight-line' );
-						} );
+					var elementsToHighlight = [];
 
+					// Highlight a range
+					if( typeof highlight.end === 'number' ) {
+						elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child(n+'+highlight.start+'):nth-child(-n+'+highlight.end+')' ) );
 					}
+					// Highlight a single line
+					else if( typeof highlight.start === 'number' ) {
+						elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child('+highlight.start+')' ) );
+					}
+
+					elementsToHighlight.forEach( function( lineElement ) {
+						lineElement.classList.add( 'highlight-line' );
+					} );
 
 				} );
 
 			}
 
+		},
+
+		/**
+		 * Parses and formats a user-defined string of line
+		 * numbers to highlight.
+		 *
+		 * @example
+		 * RevealHighlight.deserializeHighlightSteps( '1,2|3,5-10' )
+		 * // [
+		 * //   [ { start: 1 }, { start: 2 } ],
+		 * //   [ { start: 3 }, { start: 5, end: 10 } ]
+		 * // ]
+		 */
+		deserializeHighlightSteps: function( highlightSteps ) {
+
+			// Remove whitespace
+			highlightSteps = highlightSteps.replace( /\s/g, '' );
+
+			// Divide up our line number groups
+			highlightSteps = highlightSteps.split( RevealHighlight.HIGHLIGHT_STEP_DELIMITER );
+
+			return highlightSteps.map( function( highlights ) {
+
+				return highlights.split( RevealHighlight.HIGHLIGHT_LINE_DELIMITER ).map( function( highlight ) {
+
+					// Parse valid line numbers
+					if( /^[\d-]+$/.test( highlight ) ) {
+
+						highlight = highlight.split( RevealHighlight.HIGHLIGHT_LINE_RANGE_DELIMITER );
+
+						var lineStart = parseInt( highlight[0], 10 ),
+							lineEnd = parseInt( highlight[1], 10 );
+
+						if( isNaN( lineEnd ) ) {
+							return {
+								start: lineStart
+							};
+						}
+						else {
+							return {
+								start: lineStart,
+								end: lineEnd
+							};
+						}
+
+					}
+					// If no line numbers are provided, no code will be highlighted
+					else {
+
+						return {};
+
+					}
+
+				} );
+
+			} );
+
+		},
+
+		/**
+		 * Serializes parsed line number data into a string so
+		 * that we can store it in the DOM.
+		 */
+		serializeHighlightSteps: function( highlightSteps ) {
+
+			return highlightSteps.map( function( highlights ) {
+				return highlights.map( function( highlight ) {
+					if( typeof highlight.end === 'number' ) {
+						return highlight.start + RevealHighlight.HIGHLIGHT_LINE_RANGE_DELIMITER + highlight.end;
+					}
+					else if( typeof highlight.start === 'number' ) {
+						return highlight.start;
+					}
+					else {
+						return '';
+					}
+				} ).join( RevealHighlight.HIGHLIGHT_LINE_DELIMITER );
+			} ).join( RevealHighlight.HIGHLIGHT_STEP_DELIMITER );
+
 		}
+
 	}
 
 	Reveal.registerPlugin( 'highlight', RevealHighlight );