highlight.js 12 KB

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