1
0

highlight.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import hljs from './highlight-core.js'
  2. import './highlight-line-numbers.js'
  3. /*!
  4. * reveal.js plugin that adds syntax highlight support.
  5. */
  6. let Plugin = {
  7. id: 'highlight',
  8. HIGHLIGHT_STEP_DELIMITER: '|',
  9. HIGHLIGHT_LINE_DELIMITER: ',',
  10. HIGHLIGHT_LINE_RANGE_DELIMITER: '-',
  11. /**
  12. * Highlights code blocks withing the given deck.
  13. *
  14. * Note that this can be called multiple times if
  15. * there are multiple presentations on one page.
  16. *
  17. * @param {Reveal} reveal the reveal.js instance
  18. */
  19. init: function( reveal ) {
  20. // Read the plugin config options and provide fallbacks
  21. var config = reveal.getConfig().highlight || {};
  22. config.highlightOnLoad = typeof config.highlightOnLoad === 'boolean' ? config.highlightOnLoad : true;
  23. config.escapeHTML = typeof config.escapeHTML === 'boolean' ? config.escapeHTML : true;
  24. [].slice.call( reveal.getRevealElement().querySelectorAll( 'pre code' ) ).forEach( function( block ) {
  25. // Trim whitespace if the "data-trim" attribute is present
  26. if( block.hasAttribute( 'data-trim' ) && typeof block.innerHTML.trim === 'function' ) {
  27. block.innerHTML = betterTrim( block );
  28. }
  29. // Escape HTML tags unless the "data-noescape" attrbute is present
  30. if( config.escapeHTML && !block.hasAttribute( 'data-noescape' )) {
  31. block.innerHTML = block.innerHTML.replace( /</g,"&lt;").replace(/>/g, '&gt;' );
  32. }
  33. // Re-highlight when focus is lost (for contenteditable code)
  34. block.addEventListener( 'focusout', function( event ) {
  35. hljs.highlightBlock( event.currentTarget );
  36. }, false );
  37. if( config.highlightOnLoad ) {
  38. Plugin.highlightBlock( block );
  39. }
  40. } );
  41. // If we're printing to PDF, scroll the code highlights of
  42. // all blocks in the deck into view at once
  43. reveal.on( 'pdf-ready', function() {
  44. [].slice.call( reveal.getRevealElement().querySelectorAll( 'pre code[data-line-numbers].current-fragment' ) ).forEach( function( block ) {
  45. Plugin.scrollHighlightedLineIntoView( block, {}, true );
  46. } );
  47. } );
  48. },
  49. /**
  50. * Highlights a code block. If the <code> node has the
  51. * 'data-line-numbers' attribute we also generate slide
  52. * numbers.
  53. *
  54. * If the block contains multiple line highlight steps,
  55. * we clone the block and create a fragment for each step.
  56. */
  57. highlightBlock: function( block ) {
  58. hljs.highlightBlock( block );
  59. // Don't generate line numbers for empty code blocks
  60. if( block.innerHTML.trim().length === 0 ) return;
  61. if( block.hasAttribute( 'data-line-numbers' ) ) {
  62. hljs.lineNumbersBlock( block, { singleLine: true } );
  63. var scrollState = { currentBlock: block };
  64. // If there is at least one highlight step, generate
  65. // fragments
  66. var highlightSteps = Plugin.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) );
  67. if( highlightSteps.length > 1 ) {
  68. // If the original code block has a fragment-index,
  69. // each clone should follow in an incremental sequence
  70. var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 );
  71. if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) {
  72. fragmentIndex = null;
  73. }
  74. // Generate fragments for all steps except the original block
  75. highlightSteps.slice(1).forEach( function( highlight ) {
  76. var fragmentBlock = block.cloneNode( true );
  77. fragmentBlock.setAttribute( 'data-line-numbers', Plugin.serializeHighlightSteps( [ highlight ] ) );
  78. fragmentBlock.classList.add( 'fragment' );
  79. block.parentNode.appendChild( fragmentBlock );
  80. Plugin.highlightLines( fragmentBlock );
  81. if( typeof fragmentIndex === 'number' ) {
  82. fragmentBlock.setAttribute( 'data-fragment-index', fragmentIndex );
  83. fragmentIndex += 1;
  84. }
  85. else {
  86. fragmentBlock.removeAttribute( 'data-fragment-index' );
  87. }
  88. // Scroll highlights into view as we step through them
  89. fragmentBlock.addEventListener( 'visible', Plugin.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock, scrollState ) );
  90. fragmentBlock.addEventListener( 'hidden', Plugin.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock.previousSibling, scrollState ) );
  91. } );
  92. block.removeAttribute( 'data-fragment-index' )
  93. block.setAttribute( 'data-line-numbers', Plugin.serializeHighlightSteps( [ highlightSteps[0] ] ) );
  94. }
  95. // Scroll the first highlight into view when the slide
  96. // becomes visible. Note supported in IE11 since it lacks
  97. // support for Element.closest.
  98. var slide = typeof block.closest === 'function' ? block.closest( 'section:not(.stack)' ) : null;
  99. if( slide ) {
  100. var scrollFirstHighlightIntoView = function() {
  101. Plugin.scrollHighlightedLineIntoView( block, scrollState, true );
  102. slide.removeEventListener( 'visible', scrollFirstHighlightIntoView );
  103. }
  104. slide.addEventListener( 'visible', scrollFirstHighlightIntoView );
  105. }
  106. Plugin.highlightLines( block );
  107. }
  108. },
  109. /**
  110. * Animates scrolling to the first highlighted line
  111. * in the given code block.
  112. */
  113. scrollHighlightedLineIntoView: function( block, scrollState, skipAnimation ) {
  114. cancelAnimationFrame( scrollState.animationFrameID );
  115. // Match the scroll position of the currently visible
  116. // code block
  117. if( scrollState.currentBlock ) {
  118. block.scrollTop = scrollState.currentBlock.scrollTop;
  119. }
  120. // Remember the current code block so that we can match
  121. // its scroll position when showing/hiding fragments
  122. scrollState.currentBlock = block;
  123. var highlightBounds = this.getHighlightedLineBounds( block )
  124. var viewportHeight = block.offsetHeight;
  125. // Subtract padding from the viewport height
  126. var blockStyles = getComputedStyle( block );
  127. viewportHeight -= parseInt( blockStyles.paddingTop ) + parseInt( blockStyles.paddingBottom );
  128. // Scroll position which centers all highlights
  129. var startTop = block.scrollTop;
  130. var targetTop = highlightBounds.top + ( Math.min( highlightBounds.bottom - highlightBounds.top, viewportHeight ) - viewportHeight ) / 2;
  131. // Account for offsets in position applied to the
  132. // <table> that holds our lines of code
  133. var lineTable = block.querySelector( '.hljs-ln' );
  134. if( lineTable ) targetTop += lineTable.offsetTop - parseInt( blockStyles.paddingTop );
  135. // Make sure the scroll target is within bounds
  136. targetTop = Math.max( Math.min( targetTop, block.scrollHeight - viewportHeight ), 0 );
  137. if( skipAnimation === true || startTop === targetTop ) {
  138. block.scrollTop = targetTop;
  139. }
  140. else {
  141. // Don't attempt to scroll if there is no overflow
  142. if( block.scrollHeight <= viewportHeight ) return;
  143. var time = 0;
  144. var animate = function() {
  145. time = Math.min( time + 0.02, 1 );
  146. // Update our eased scroll position
  147. block.scrollTop = startTop + ( targetTop - startTop ) * Plugin.easeInOutQuart( time );
  148. // Keep animating unless we've reached the end
  149. if( time < 1 ) {
  150. scrollState.animationFrameID = requestAnimationFrame( animate );
  151. }
  152. };
  153. animate();
  154. }
  155. },
  156. /**
  157. * The easing function used when scrolling.
  158. */
  159. easeInOutQuart: function( t ) {
  160. // easeInOutQuart
  161. return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t;
  162. },
  163. getHighlightedLineBounds: function( block ) {
  164. var highlightedLines = block.querySelectorAll( '.highlight-line' );
  165. if( highlightedLines.length === 0 ) {
  166. return { top: 0, bottom: 0 };
  167. }
  168. else {
  169. var firstHighlight = highlightedLines[0];
  170. var lastHighlight = highlightedLines[ highlightedLines.length -1 ];
  171. return {
  172. top: firstHighlight.offsetTop,
  173. bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight
  174. }
  175. }
  176. },
  177. /**
  178. * Visually emphasize specific lines within a code block.
  179. * This only works on blocks with line numbering turned on.
  180. *
  181. * @param {HTMLElement} block a <code> block
  182. * @param {String} [linesToHighlight] The lines that should be
  183. * highlighted in this format:
  184. * "1" = highlights line 1
  185. * "2,5" = highlights lines 2 & 5
  186. * "2,5-7" = highlights lines 2, 5, 6 & 7
  187. */
  188. highlightLines: function( block, linesToHighlight ) {
  189. var highlightSteps = Plugin.deserializeHighlightSteps( linesToHighlight || block.getAttribute( 'data-line-numbers' ) );
  190. if( highlightSteps.length ) {
  191. highlightSteps[0].forEach( function( highlight ) {
  192. var elementsToHighlight = [];
  193. // Highlight a range
  194. if( typeof highlight.end === 'number' ) {
  195. elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child(n+'+highlight.start+'):nth-child(-n+'+highlight.end+')' ) );
  196. }
  197. // Highlight a single line
  198. else if( typeof highlight.start === 'number' ) {
  199. elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child('+highlight.start+')' ) );
  200. }
  201. if( elementsToHighlight.length ) {
  202. elementsToHighlight.forEach( function( lineElement ) {
  203. lineElement.classList.add( 'highlight-line' );
  204. } );
  205. block.classList.add( 'has-highlights' );
  206. }
  207. } );
  208. }
  209. },
  210. /**
  211. * Parses and formats a user-defined string of line
  212. * numbers to highlight.
  213. *
  214. * @example
  215. * Plugin.deserializeHighlightSteps( '1,2|3,5-10' )
  216. * // [
  217. * // [ { start: 1 }, { start: 2 } ],
  218. * // [ { start: 3 }, { start: 5, end: 10 } ]
  219. * // ]
  220. */
  221. deserializeHighlightSteps: function( highlightSteps ) {
  222. // Remove whitespace
  223. highlightSteps = highlightSteps.replace( /\s/g, '' );
  224. // Divide up our line number groups
  225. highlightSteps = highlightSteps.split( Plugin.HIGHLIGHT_STEP_DELIMITER );
  226. return highlightSteps.map( function( highlights ) {
  227. return highlights.split( Plugin.HIGHLIGHT_LINE_DELIMITER ).map( function( highlight ) {
  228. // Parse valid line numbers
  229. if( /^[\d-]+$/.test( highlight ) ) {
  230. highlight = highlight.split( Plugin.HIGHLIGHT_LINE_RANGE_DELIMITER );
  231. var lineStart = parseInt( highlight[0], 10 ),
  232. lineEnd = parseInt( highlight[1], 10 );
  233. if( isNaN( lineEnd ) ) {
  234. return {
  235. start: lineStart
  236. };
  237. }
  238. else {
  239. return {
  240. start: lineStart,
  241. end: lineEnd
  242. };
  243. }
  244. }
  245. // If no line numbers are provided, no code will be highlighted
  246. else {
  247. return {};
  248. }
  249. } );
  250. } );
  251. },
  252. /**
  253. * Serializes parsed line number data into a string so
  254. * that we can store it in the DOM.
  255. */
  256. serializeHighlightSteps: function( highlightSteps ) {
  257. return highlightSteps.map( function( highlights ) {
  258. return highlights.map( function( highlight ) {
  259. // Line range
  260. if( typeof highlight.end === 'number' ) {
  261. return highlight.start + Plugin.HIGHLIGHT_LINE_RANGE_DELIMITER + highlight.end;
  262. }
  263. // Single line
  264. else if( typeof highlight.start === 'number' ) {
  265. return highlight.start;
  266. }
  267. // All lines
  268. else {
  269. return '';
  270. }
  271. } ).join( Plugin.HIGHLIGHT_LINE_DELIMITER );
  272. } ).join( Plugin.HIGHLIGHT_STEP_DELIMITER );
  273. }
  274. }
  275. // Function to perform a better "data-trim" on code snippets
  276. // Will slice an indentation amount on each line of the snippet (amount based on the line having the lowest indentation length)
  277. function betterTrim(snippetEl) {
  278. // Helper functions
  279. function trimLeft(val) {
  280. // Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill
  281. return val.replace(/^[\s\uFEFF\xA0]+/g, '');
  282. }
  283. function trimLineBreaks(input) {
  284. var lines = input.split('\n');
  285. // Trim line-breaks from the beginning
  286. for (var i = 0; i < lines.length; i++) {
  287. if (lines[i].trim() === '') {
  288. lines.splice(i--, 1);
  289. } else break;
  290. }
  291. // Trim line-breaks from the end
  292. for (var i = lines.length-1; i >= 0; i--) {
  293. if (lines[i].trim() === '') {
  294. lines.splice(i, 1);
  295. } else break;
  296. }
  297. return lines.join('\n');
  298. }
  299. // Main function for betterTrim()
  300. return (function(snippetEl) {
  301. var content = trimLineBreaks(snippetEl.innerHTML);
  302. var lines = content.split('\n');
  303. // Calculate the minimum amount to remove on each line start of the snippet (can be 0)
  304. var pad = lines.reduce(function(acc, line) {
  305. if (line.length > 0 && trimLeft(line).length > 0 && acc > line.length - trimLeft(line).length) {
  306. return line.length - trimLeft(line).length;
  307. }
  308. return acc;
  309. }, Number.POSITIVE_INFINITY);
  310. // Slice each line with this amount
  311. return lines.map(function(line, index) {
  312. return line.slice(pad);
  313. })
  314. .join('\n');
  315. })(snippetEl);
  316. }
  317. export default () => Plugin;