markdown.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. /*!
  2. * The reveal.js markdown plugin. Handles parsing of
  3. * markdown inside of presentations as well as loading
  4. * of external markdown documents.
  5. */
  6. import marked from './marked.js'
  7. let Plugin = {
  8. id: 'markdown',
  9. /**
  10. * Starts processing and converting Markdown within the
  11. * current reveal.js deck.
  12. */
  13. init: function( deck ) {
  14. // This should no longer be needed, as long as the highlight.js
  15. // plugin is included after the markdown plugin
  16. // if( typeof window.hljs !== 'undefined' ) {
  17. // marked.setOptions({
  18. // highlight: function( code, lang ) {
  19. // return window.hljs.highlightAuto( code, lang ? [lang] : null ).value;
  20. // }
  21. // });
  22. // }
  23. // marked can be configured via reveal.js config options
  24. var options = deck.getConfig().markdown;
  25. if( options ) {
  26. marked.setOptions( options );
  27. }
  28. return processSlides( deck.getRevealElement() ).then( convertSlides );
  29. },
  30. // TODO: Do these belong in the API?
  31. processSlides: processSlides,
  32. convertSlides: convertSlides,
  33. slidify: slidify,
  34. marked: marked
  35. };
  36. export default () => Plugin;
  37. var DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$',
  38. DEFAULT_NOTES_SEPARATOR = 'notes?:',
  39. DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
  40. DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
  41. var SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
  42. /**
  43. * Retrieves the markdown contents of a slide section
  44. * element. Normalizes leading tabs/whitespace.
  45. */
  46. function getMarkdownFromSlide( section ) {
  47. // look for a <script> or <textarea data-template> wrapper
  48. var template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' );
  49. // strip leading whitespace so it isn't evaluated as code
  50. var text = ( template || section ).textContent;
  51. // restore script end tags
  52. text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' );
  53. var leadingWs = text.match( /^\n?(\s*)/ )[1].length,
  54. leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
  55. if( leadingTabs > 0 ) {
  56. text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}','g'), '\n' );
  57. }
  58. else if( leadingWs > 1 ) {
  59. text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' );
  60. }
  61. return text;
  62. }
  63. /**
  64. * Given a markdown slide section element, this will
  65. * return all arguments that aren't related to markdown
  66. * parsing. Used to forward any other user-defined arguments
  67. * to the output markdown slide.
  68. */
  69. function getForwardedAttributes( section ) {
  70. var attributes = section.attributes;
  71. var result = [];
  72. for( var i = 0, len = attributes.length; i < len; i++ ) {
  73. var name = attributes[i].name,
  74. value = attributes[i].value;
  75. // disregard attributes that are used for markdown loading/parsing
  76. if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
  77. if( value ) {
  78. result.push( name + '="' + value + '"' );
  79. }
  80. else {
  81. result.push( name );
  82. }
  83. }
  84. return result.join( ' ' );
  85. }
  86. /**
  87. * Inspects the given options and fills out default
  88. * values for what's not defined.
  89. */
  90. function getSlidifyOptions( options ) {
  91. options = options || {};
  92. options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR;
  93. options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR;
  94. options.attributes = options.attributes || '';
  95. return options;
  96. }
  97. /**
  98. * Helper function for constructing a markdown slide.
  99. */
  100. function createMarkdownSlide( content, options ) {
  101. options = getSlidifyOptions( options );
  102. var notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
  103. if( notesMatch.length === 2 ) {
  104. content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>';
  105. }
  106. // prevent script end tags in the content from interfering
  107. // with parsing
  108. content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
  109. return '<script type="text/template">' + content + '</script>';
  110. }
  111. /**
  112. * Parses a data string into multiple slides based
  113. * on the passed in separator arguments.
  114. */
  115. function slidify( markdown, options ) {
  116. options = getSlidifyOptions( options );
  117. var separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
  118. horizontalSeparatorRegex = new RegExp( options.separator );
  119. var matches,
  120. lastIndex = 0,
  121. isHorizontal,
  122. wasHorizontal = true,
  123. content,
  124. sectionStack = [];
  125. // iterate until all blocks between separators are stacked up
  126. while( matches = separatorRegex.exec( markdown ) ) {
  127. var notes = null;
  128. // determine direction (horizontal by default)
  129. isHorizontal = horizontalSeparatorRegex.test( matches[0] );
  130. if( !isHorizontal && wasHorizontal ) {
  131. // create vertical stack
  132. sectionStack.push( [] );
  133. }
  134. // pluck slide content from markdown input
  135. content = markdown.substring( lastIndex, matches.index );
  136. if( isHorizontal && wasHorizontal ) {
  137. // add to horizontal stack
  138. sectionStack.push( content );
  139. }
  140. else {
  141. // add to vertical stack
  142. sectionStack[sectionStack.length-1].push( content );
  143. }
  144. lastIndex = separatorRegex.lastIndex;
  145. wasHorizontal = isHorizontal;
  146. }
  147. // add the remaining slide
  148. ( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
  149. var markdownSections = '';
  150. // flatten the hierarchical stack, and insert <section data-markdown> tags
  151. for( var i = 0, len = sectionStack.length; i < len; i++ ) {
  152. // vertical
  153. if( sectionStack[i] instanceof Array ) {
  154. markdownSections += '<section '+ options.attributes +'>';
  155. sectionStack[i].forEach( function( child ) {
  156. markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>';
  157. } );
  158. markdownSections += '</section>';
  159. }
  160. else {
  161. markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>';
  162. }
  163. }
  164. return markdownSections;
  165. }
  166. /**
  167. * Parses any current data-markdown slides, splits
  168. * multi-slide markdown into separate sections and
  169. * handles loading of external markdown.
  170. */
  171. function processSlides( scope ) {
  172. return new Promise( function( resolve ) {
  173. var externalPromises = [];
  174. [].slice.call( scope.querySelectorAll( '[data-markdown]:not([data-markdown-parsed])') ).forEach( function( section, i ) {
  175. if( section.getAttribute( 'data-markdown' ).length ) {
  176. externalPromises.push( loadExternalMarkdown( section ).then(
  177. // Finished loading external file
  178. function( xhr, url ) {
  179. section.outerHTML = slidify( xhr.responseText, {
  180. separator: section.getAttribute( 'data-separator' ),
  181. verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
  182. notesSeparator: section.getAttribute( 'data-separator-notes' ),
  183. attributes: getForwardedAttributes( section )
  184. });
  185. },
  186. // Failed to load markdown
  187. function( xhr, url ) {
  188. section.outerHTML = '<section data-state="alert">' +
  189. 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
  190. 'Check your browser\'s JavaScript console for more details.' +
  191. '<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
  192. '</section>';
  193. }
  194. ) );
  195. }
  196. else if( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) {
  197. section.outerHTML = slidify( getMarkdownFromSlide( section ), {
  198. separator: section.getAttribute( 'data-separator' ),
  199. verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
  200. notesSeparator: section.getAttribute( 'data-separator-notes' ),
  201. attributes: getForwardedAttributes( section )
  202. });
  203. }
  204. else {
  205. section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) );
  206. }
  207. });
  208. Promise.all( externalPromises ).then( resolve );
  209. } );
  210. }
  211. function loadExternalMarkdown( section ) {
  212. return new Promise( function( resolve, reject ) {
  213. var xhr = new XMLHttpRequest(),
  214. url = section.getAttribute( 'data-markdown' );
  215. var datacharset = section.getAttribute( 'data-charset' );
  216. // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
  217. if( datacharset != null && datacharset != '' ) {
  218. xhr.overrideMimeType( 'text/html; charset=' + datacharset );
  219. }
  220. xhr.onreadystatechange = function( section, xhr ) {
  221. if( xhr.readyState === 4 ) {
  222. // file protocol yields status code 0 (useful for local debug, mobile applications etc.)
  223. if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
  224. resolve( xhr, url );
  225. }
  226. else {
  227. reject( xhr, url );
  228. }
  229. }
  230. }.bind( this, section, xhr );
  231. xhr.open( 'GET', url, true );
  232. try {
  233. xhr.send();
  234. }
  235. catch ( e ) {
  236. console.warn( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
  237. resolve( xhr, url );
  238. }
  239. } );
  240. }
  241. /**
  242. * Check if a node value has the attributes pattern.
  243. * If yes, extract it and add that value as one or several attributes
  244. * to the target element.
  245. *
  246. * You need Cache Killer on Chrome to see the effect on any FOM transformation
  247. * directly on refresh (F5)
  248. * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
  249. */
  250. function addAttributeInElement( node, elementTarget, separator ) {
  251. var mardownClassesInElementsRegex = new RegExp( separator, 'mg' );
  252. var mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"]+?)\"|(data-[^\"= ]+?)(?=[\" ])", 'mg' );
  253. var nodeValue = node.nodeValue;
  254. var matches,
  255. matchesClass;
  256. if( matches = mardownClassesInElementsRegex.exec( nodeValue ) ) {
  257. var classes = matches[1];
  258. nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex );
  259. node.nodeValue = nodeValue;
  260. while( matchesClass = mardownClassRegex.exec( classes ) ) {
  261. if( matchesClass[2] ) {
  262. elementTarget.setAttribute( matchesClass[1], matchesClass[2] );
  263. } else {
  264. elementTarget.setAttribute( matchesClass[3], "" );
  265. }
  266. }
  267. return true;
  268. }
  269. return false;
  270. }
  271. /**
  272. * Add attributes to the parent element of a text node,
  273. * or the element of an attribute node.
  274. */
  275. function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) {
  276. if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) {
  277. var previousParentElement = element;
  278. for( var i = 0; i < element.childNodes.length; i++ ) {
  279. var childElement = element.childNodes[i];
  280. if ( i > 0 ) {
  281. var j = i - 1;
  282. while ( j >= 0 ) {
  283. var aPreviousChildElement = element.childNodes[j];
  284. if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != "BR" ) {
  285. previousParentElement = aPreviousChildElement;
  286. break;
  287. }
  288. j = j - 1;
  289. }
  290. }
  291. var parentSection = section;
  292. if( childElement.nodeName == "section" ) {
  293. parentSection = childElement ;
  294. previousParentElement = childElement ;
  295. }
  296. if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) {
  297. addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes );
  298. }
  299. }
  300. }
  301. if ( element.nodeType == Node.COMMENT_NODE ) {
  302. if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) {
  303. addAttributeInElement( element, section, separatorSectionAttributes );
  304. }
  305. }
  306. }
  307. /**
  308. * Converts any current data-markdown slides in the
  309. * DOM to HTML.
  310. */
  311. function convertSlides() {
  312. var sections = document.querySelectorAll( '[data-markdown]:not([data-markdown-parsed])');
  313. [].slice.call( sections ).forEach( function( section ) {
  314. section.setAttribute( 'data-markdown-parsed', true )
  315. var notes = section.querySelector( 'aside.notes' );
  316. var markdown = getMarkdownFromSlide( section );
  317. section.innerHTML = marked( markdown );
  318. addAttributes( section, section, null, section.getAttribute( 'data-element-attributes' ) ||
  319. section.parentNode.getAttribute( 'data-element-attributes' ) ||
  320. DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
  321. section.getAttribute( 'data-attributes' ) ||
  322. section.parentNode.getAttribute( 'data-attributes' ) ||
  323. DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
  324. // If there were notes, we need to re-add them after
  325. // having overwritten the section's HTML
  326. if( notes ) {
  327. section.appendChild( notes );
  328. }
  329. } );
  330. return Promise.resolve();
  331. }