diff --git a/css/reveal.css b/css/reveal.css index 1b9651b5817..d549340d218 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 ab732a46116..0b7718a6d80 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 f88bfa2697e..cf05e88141e 100644 --- a/demo.html +++ b/demo.html @@ -241,7 +241,7 @@

Iframe Backgrounds

Pretty Code

-

+					

 import React, { useState } from 'react';
 
 function Example() {
@@ -412,7 +412,7 @@ 

THE END

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 f938be54883..a4825a98654 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 83305e11486..b2712614c54 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, '' ); + var elementsToHighlight = []; - // Ensure that we looking at a valid slide number (1 or 1-2) - if( /^[\d-]+$/.test( lineNumbers ) ) { + // 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+')' ) ); + } - lineNumbers = lineNumbers.split( '-' ); + elementsToHighlight.forEach( function( lineElement ) { + lineElement.classList.add( 'highlight-line' ); + } ); - 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' ); - } ); + } + + }, + + /** + * 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 );