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