lib/asciidoctor-revealjs/highlightjs.rb in asciidoctor-revealjs-4.1.0 vs lib/asciidoctor-revealjs/highlightjs.rb in asciidoctor-revealjs-5.0.0.rc1

- old
+ new

@@ -4,11 +4,14 @@ module SyntaxHighlighter # Override the built-in highlight.js syntax highlighter class HighlightJsAdapter < Asciidoctor::SyntaxHighlighter::Base register_for 'highlightjs', 'highlight.js' - HIGHLIGHT_JS_VERSION = '9.18.1' + # REMIND: we cannot use Highlight.js 11+ because unescaped HTML support has been removed: + # https://github.com/highlightjs/highlight.js/issues/2889 + # We are using unescaped HTML in source blocks for callout. + HIGHLIGHT_JS_VERSION = '10.7.3' def initialize *args super @name = @pre_class = 'highlightjs' end @@ -17,23 +20,29 @@ # In addition to Asciidoctor's linenum converter leveraging core's resolve_lines_to_highlight, # we also support reveal.js step-by-step highlights. # The steps are split using the | character # For example, this method makes "1..3|6,7" into "1,2,3|6,7" def _convert_highlight_to_revealjs node - return node.attributes["highlight"].split("|").collect { |linenums| - node.resolve_lines_to_highlight(node.content, linenums).join(",") - }.join("|") + node.attributes['highlight'].split('|').collect { |linenums| + node.resolve_lines_to_highlight(node.content, linenums).join(',') + }.join('|') end def format node, lang, opts - super node, lang, (opts.merge transform: proc { |_, code| + super node, lang, (opts.merge transform: proc { |pre, code| code['class'] = %(language-#{lang || 'none'} hljs) code['data-noescape'] = true + if (id = node.attr('data-id')) + pre['data-id'] = id + end + if node.option?('trim') + code['data-trim'] = '' + end - if node.attributes.key?("highlight") + if node.attributes.key?('highlight') code['data-line-numbers'] = self._convert_highlight_to_revealjs(node) - elsif node.attributes.key?("linenums") + elsif node.attributes.key?('linenums') code['data-line-numbers'] = '' end }) end @@ -48,23 +57,26 @@ revealjsdir = (doc.attr :revealjsdir, 'reveal.js') end if doc.attr? 'highlightjs-theme' theme_href = doc.attr 'highlightjs-theme' else - theme_href = "#{revealjsdir}/lib/css/monokai.css" + theme_href = "#{revealjsdir}/plugin/highlight/monokai.css" end base_url = doc.attr 'highlightjsdir', %(#{opts[:cdn_base_url]}/highlight.js/#{HIGHLIGHT_JS_VERSION}) %(<link rel="stylesheet" href="#{theme_href}"#{opts[:self_closing_tag_slash]}> <script src="#{base_url}/highlight.min.js"></script> #{(doc.attr? 'highlightjs-languages') ? ((doc.attr 'highlightjs-languages').split ',').map {|lang| %[<script src="#{base_url}/languages/#{lang.lstrip}.min.js"></script>\n] }.join : ''} <script> #{HIGHLIGHT_PLUGIN_SOURCE} -hljs.initHighlightingOnLoad(); +hljs.configure({ + ignoreUnescapedHTML: true, +}); +hljs.highlightAll(); </script>) end - # this file was copied-pasted from https://raw.githubusercontent.com/hakimel/reveal.js/3.9.2/plugin/highlight/highlight.js + # this file was copied-pasted from https://raw.githubusercontent.com/hakimel/reveal.js/4.1.2/plugin/highlight/plugin.js # please note that the bundled highlight.js code was removed so we can use the latest version from cdnjs. HIGHLIGHT_PLUGIN_SOURCE = %q{ /* highlightjs-line-numbers.js 2.6.0 | (C) 2018 Yauheni Pakala | MIT License | github.com/wcoder/highlightjs-line-numbers.js */ /* Edited by Hakim for reveal.js; removed async timeout */ !function(n,e){"use strict";function t(){var n=e.createElement("style");n.type="text/css",n.innerHTML=g(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[v,L,b]),e.getElementsByTagName("head")[0].appendChild(n)}function r(t){"interactive"===e.readyState||"complete"===e.readyState?i(t):n.addEventListener("DOMContentLoaded",function(){i(t)})}function i(t){try{var r=e.querySelectorAll("code.hljs,code.nohighlight");for(var i in r)r.hasOwnProperty(i)&&l(r[i],t)}catch(o){n.console.error("LineNumbers error: ",o)}}function l(n,e){"object"==typeof n&&f(function(){n.innerHTML=s(n,e)})}function o(n,e){if("string"==typeof n){var t=document.createElement("code");return t.innerHTML=n,s(t,e)}}function s(n,e){e=e||{singleLine:!1};var t=e.singleLine?0:1;return c(n),a(n.innerHTML,t)}function a(n,e){var t=u(n);if(""===t[t.length-1].trim()&&t.pop(),t.length>e){for(var r="",i=0,l=t.length;i<l;i++)r+=g('<tr><td class="{0}"><div class="{1} {2}" {3}="{5}"></div></td><td class="{4}"><div class="{1}">{6}</div></td></tr>',[j,m,L,b,p,i+1,t[i].length>0?t[i]:" "]);return g('<table class="{0}">{1}</table>',[v,r])}return n}function c(n){var e=n.childNodes;for(var t in e)if(e.hasOwnProperty(t)){var r=e[t];h(r.textContent)>0&&(r.childNodes.length>0?c(r):d(r.parentNode))}}function d(n){var e=n.className;if(/hljs-/.test(e)){for(var t=u(n.innerHTML),r=0,i="";r<t.length;r++){var l=t[r].length>0?t[r]:" ";i+=g('<span class="{0}">{1}</span>\n',[e,l])}n.innerHTML=i.trim()}}function u(n){return 0===n.length?[]:n.split(y)}function h(n){return(n.trim().match(y)||[]).length}function f(e){e()}function g(n,e){return n.replace(/\{(\d+)\}/g,function(n,t){return e[t]?e[t]:n})}var v="hljs-ln",m="hljs-ln-line",p="hljs-ln-code",j="hljs-ln-numbers",L="hljs-ln-n",b="data-line-number",y=/\r\n|\r|\n/g;n.hljs?(n.hljs.initLineNumbersOnLoad=r,n.hljs.lineNumbersBlock=l,n.hljs.lineNumbersValue=o,t()):n.console.error("highlight.js not detected!")}(window,document); @@ -135,19 +147,30 @@ HIGHLIGHT_STEP_DELIMITER: '|', HIGHLIGHT_LINE_DELIMITER: ',', HIGHLIGHT_LINE_RANGE_DELIMITER: '-', - init: function() { + init: function( reveal ) { // Read the plugin config options and provide fallbacks var config = Reveal.getConfig().highlight || {}; config.highlightOnLoad = typeof config.highlightOnLoad === 'boolean' ? config.highlightOnLoad : true; config.escapeHTML = typeof config.escapeHTML === 'boolean' ? config.escapeHTML : true; - [].slice.call( document.querySelectorAll( '.reveal pre code' ) ).forEach( function( block ) { + [].slice.call( reveal.getRevealElement().querySelectorAll( 'pre code' ) ).forEach( function( block ) { + block.parentNode.className = 'code-wrapper'; + + // Code can optionally be wrapped in script template to avoid + // HTML being parsed by the browser (i.e. when you need to + // include <, > or & in your code). + let substitute = block.querySelector( 'script[type="text/template"]' ); + if( substitute ) { + // textContent handles the HTML entity escapes for us + block.textContent = substitute.innerHTML; + } + // Trim whitespace if the "data-trim" attribute is present if( block.hasAttribute( 'data-trim' ) && typeof block.innerHTML.trim === 'function' ) { block.innerHTML = betterTrim( block ); } @@ -156,18 +179,25 @@ block.innerHTML = block.innerHTML.replace( /</g,"&lt;").replace(/>/g, '&gt;' ); } // Re-highlight when focus is lost (for contenteditable code) block.addEventListener( 'focusout', function( event ) { - hljs.highlightBlock( event.currentTarget ); + hljs.highlightElement( event.currentTarget ); }, false ); if( config.highlightOnLoad ) { RevealHighlight.highlightBlock( block ); } } ); + // If we're printing to PDF, scroll the code highlights of + // all blocks in the deck into view at once + reveal.on( 'pdf-ready', function() { + [].slice.call( reveal.getRevealElement().querySelectorAll( 'pre code[data-line-numbers].current-fragment' ) ).forEach( function( block ) { + RevealHighlight.scrollHighlightedLineIntoView( block, {}, true ); + } ); + } ); }, /** * Highlights a code block. If the <code> node has the * 'data-line-numbers' attribute we also generate slide @@ -176,26 +206,29 @@ * If the block contains multiple line highlight steps, * we clone the block and create a fragment for each step. */ highlightBlock: function( block ) { - hljs.highlightBlock( block ); + hljs.highlightElement( block ); // Don't generate line numbers for empty code blocks if( block.innerHTML.trim().length === 0 ) return; if( block.hasAttribute( 'data-line-numbers' ) ) { hljs.lineNumbersBlock( block, { singleLine: true } ); + var scrollState = { currentBlock: block }; + // If there is at least one highlight step, generate // fragments var highlightSteps = RevealHighlight.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) ); if( highlightSteps.length > 1 ) { // If the original code block has a fragment-index, // each clone should follow in an incremental sequence var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 ); + if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) { fragmentIndex = null; } // Generate fragments for all steps except the original block @@ -213,18 +246,126 @@ } else { fragmentBlock.removeAttribute( 'data-fragment-index' ); } + // Scroll highlights into view as we step through them + fragmentBlock.addEventListener( 'visible', RevealHighlight.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock, scrollState ) ); + fragmentBlock.addEventListener( 'hidden', RevealHighlight.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock.previousSibling, scrollState ) ); + } ); block.removeAttribute( 'data-fragment-index' ) block.setAttribute( 'data-line-numbers', RevealHighlight.serializeHighlightSteps( [ highlightSteps[0] ] ) ); } + // Scroll the first highlight into view when the slide + // becomes visible. Note supported in IE11 since it lacks + // support for Element.closest. + var slide = typeof block.closest === 'function' ? block.closest( 'section:not(.stack)' ) : null; + if( slide ) { + var scrollFirstHighlightIntoView = function() { + RevealHighlight.scrollHighlightedLineIntoView( block, scrollState, true ); + slide.removeEventListener( 'visible', scrollFirstHighlightIntoView ); + } + slide.addEventListener( 'visible', scrollFirstHighlightIntoView ); + } + RevealHighlight.highlightLines( block ); + } + + }, + + /** + * Animates scrolling to the first highlighted line + * in the given code block. + */ + scrollHighlightedLineIntoView: function( block, scrollState, skipAnimation ) { + + cancelAnimationFrame( scrollState.animationFrameID ); + + // Match the scroll position of the currently visible + // code block + if( scrollState.currentBlock ) { + block.scrollTop = scrollState.currentBlock.scrollTop; + } + + // Remember the current code block so that we can match + // its scroll position when showing/hiding fragments + scrollState.currentBlock = block; + + var highlightBounds = RevealHighlight.getHighlightedLineBounds( block ) + var viewportHeight = block.offsetHeight; + + // Subtract padding from the viewport height + var blockStyles = getComputedStyle( block ); + viewportHeight -= parseInt( blockStyles.paddingTop ) + parseInt( blockStyles.paddingBottom ); + + // Scroll position which centers all highlights + var startTop = block.scrollTop; + var targetTop = highlightBounds.top + ( Math.min( highlightBounds.bottom - highlightBounds.top, viewportHeight ) - viewportHeight ) / 2; + + // Account for offsets in position applied to the + // <table> that holds our lines of code + var lineTable = block.querySelector( '.hljs-ln' ); + if( lineTable ) targetTop += lineTable.offsetTop - parseInt( blockStyles.paddingTop ); + + // Make sure the scroll target is within bounds + targetTop = Math.max( Math.min( targetTop, block.scrollHeight - viewportHeight ), 0 ); + + if( skipAnimation === true || startTop === targetTop ) { + block.scrollTop = targetTop; + } + else { + + // Don't attempt to scroll if there is no overflow + if( block.scrollHeight <= viewportHeight ) return; + + var time = 0; + var animate = function() { + time = Math.min( time + 0.02, 1 ); + + // Update our eased scroll position + block.scrollTop = startTop + ( targetTop - startTop ) * RevealHighlight.easeInOutQuart( time ); + + // Keep animating unless we've reached the end + if( time < 1 ) { + scrollState.animationFrameID = requestAnimationFrame( animate ); + } + }; + + animate(); + + } + + }, + + /** + * The easing function used when scrolling. + */ + easeInOutQuart: function( t ) { + + // easeInOutQuart + return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t; + + }, + + getHighlightedLineBounds: function( block ) { + + var highlightedLines = block.querySelectorAll( '.highlight-line' ); + if( highlightedLines.length === 0 ) { + return { top: 0, bottom: 0 }; + } + else { + var firstHighlight = highlightedLines[0]; + var lastHighlight = highlightedLines[ highlightedLines.length -1 ]; + + return { + top: firstHighlight.offsetTop, + bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight + } } }, /**