all repos — slides @ dd23301e4567c63669a7d53d084ef399ac52f436

Reveal-md slides I made for various occasions

réalise/_static/plugin/markdown/plugin.js (view raw)

  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
  7import { marked } from 'marked';
  8
  9const DEFAULT_SLIDE_SEPARATOR = '\r?\n---\r?\n',
 10	  DEFAULT_NOTES_SEPARATOR = 'notes?:',
 11	  DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
 12	  DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
 13
 14const SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
 15
 16const CODE_LINE_NUMBER_REGEX = /\[([\s\d,|-]*)\]/;
 17
 18const HTML_ESCAPE_MAP = {
 19  '&': '&',
 20  '<': '&lt;',
 21  '>': '&gt;',
 22  '"': '&quot;',
 23  "'": '&#39;'
 24};
 25
 26const Plugin = () => {
 27
 28	// The reveal.js instance this plugin is attached to
 29	let deck;
 30
 31	/**
 32	 * Retrieves the markdown contents of a slide section
 33	 * element. Normalizes leading tabs/whitespace.
 34	 */
 35	function getMarkdownFromSlide( section ) {
 36
 37		// look for a <script> or <textarea data-template> wrapper
 38		var template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' );
 39
 40		// strip leading whitespace so it isn't evaluated as code
 41		var text = ( template || section ).textContent;
 42
 43		// restore script end tags
 44		text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' );
 45
 46		var leadingWs = text.match( /^\n?(\s*)/ )[1].length,
 47			leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
 48
 49		if( leadingTabs > 0 ) {
 50			text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}(.*)','g'), function(m, p1) { return '\n' + p1 ; } );
 51		}
 52		else if( leadingWs > 1 ) {
 53			text = text.replace( new RegExp('\\n? {' + leadingWs + '}(.*)', 'g'), function(m, p1) { return '\n' + p1 ; } );
 54		}
 55
 56		return text;
 57
 58	}
 59
 60	/**
 61	 * Given a markdown slide section element, this will
 62	 * return all arguments that aren't related to markdown
 63	 * parsing. Used to forward any other user-defined arguments
 64	 * to the output markdown slide.
 65	 */
 66	function getForwardedAttributes( section ) {
 67
 68		var attributes = section.attributes;
 69		var result = [];
 70
 71		for( var i = 0, len = attributes.length; i < len; i++ ) {
 72			var name = attributes[i].name,
 73				value = attributes[i].value;
 74
 75			// disregard attributes that are used for markdown loading/parsing
 76			if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
 77
 78			if( value ) {
 79				result.push( name + '="' + value + '"' );
 80			}
 81			else {
 82				result.push( name );
 83			}
 84		}
 85
 86		return result.join( ' ' );
 87
 88	}
 89
 90	/**
 91	 * Inspects the given options and fills out default
 92	 * values for what's not defined.
 93	 */
 94	function getSlidifyOptions( options ) {
 95
 96		options = options || {};
 97		options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR;
 98		options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR;
 99		options.attributes = options.attributes || '';
100
101		return options;
102
103	}
104
105	/**
106	 * Helper function for constructing a markdown slide.
107	 */
108	function createMarkdownSlide( content, options ) {
109
110		options = getSlidifyOptions( options );
111
112		var notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
113
114		if( notesMatch.length === 2 ) {
115			content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>';
116		}
117
118		// prevent script end tags in the content from interfering
119		// with parsing
120		content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
121
122		return '<script type="text/template">' + content + '</script>';
123
124	}
125
126	/**
127	 * Parses a data string into multiple slides based
128	 * on the passed in separator arguments.
129	 */
130	function slidify( markdown, options ) {
131
132		options = getSlidifyOptions( options );
133
134		var separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
135			horizontalSeparatorRegex = new RegExp( options.separator );
136
137		var matches,
138			lastIndex = 0,
139			isHorizontal,
140			wasHorizontal = true,
141			content,
142			sectionStack = [];
143
144		// iterate until all blocks between separators are stacked up
145		while( matches = separatorRegex.exec( markdown ) ) {
146			var notes = null;
147
148			// determine direction (horizontal by default)
149			isHorizontal = horizontalSeparatorRegex.test( matches[0] );
150
151			if( !isHorizontal && wasHorizontal ) {
152				// create vertical stack
153				sectionStack.push( [] );
154			}
155
156			// pluck slide content from markdown input
157			content = markdown.substring( lastIndex, matches.index );
158
159			if( isHorizontal && wasHorizontal ) {
160				// add to horizontal stack
161				sectionStack.push( content );
162			}
163			else {
164				// add to vertical stack
165				sectionStack[sectionStack.length-1].push( content );
166			}
167
168			lastIndex = separatorRegex.lastIndex;
169			wasHorizontal = isHorizontal;
170		}
171
172		// add the remaining slide
173		( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
174
175		var markdownSections = '';
176
177		// flatten the hierarchical stack, and insert <section data-markdown> tags
178		for( var i = 0, len = sectionStack.length; i < len; i++ ) {
179			// vertical
180			if( sectionStack[i] instanceof Array ) {
181				markdownSections += '<section '+ options.attributes +'>';
182
183				sectionStack[i].forEach( function( child ) {
184					markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>';
185				} );
186
187				markdownSections += '</section>';
188			}
189			else {
190				markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>';
191			}
192		}
193
194		return markdownSections;
195
196	}
197
198	/**
199	 * Parses any current data-markdown slides, splits
200	 * multi-slide markdown into separate sections and
201	 * handles loading of external markdown.
202	 */
203	function processSlides( scope ) {
204
205		return new Promise( function( resolve ) {
206
207			var externalPromises = [];
208
209			[].slice.call( scope.querySelectorAll( 'section[data-markdown]:not([data-markdown-parsed])') ).forEach( function( section, i ) {
210
211				if( section.getAttribute( 'data-markdown' ).length ) {
212
213					externalPromises.push( loadExternalMarkdown( section ).then(
214
215						// Finished loading external file
216						function( xhr, url ) {
217							section.outerHTML = slidify( xhr.responseText, {
218								separator: section.getAttribute( 'data-separator' ),
219								verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
220								notesSeparator: section.getAttribute( 'data-separator-notes' ),
221								attributes: getForwardedAttributes( section )
222							});
223						},
224
225						// Failed to load markdown
226						function( xhr, url ) {
227							section.outerHTML = '<section data-state="alert">' +
228								'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
229								'Check your browser\'s JavaScript console for more details.' +
230								'<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
231								'</section>';
232						}
233
234					) );
235
236				}
237				else {
238
239					section.outerHTML = slidify( getMarkdownFromSlide( section ), {
240						separator: section.getAttribute( 'data-separator' ),
241						verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
242						notesSeparator: section.getAttribute( 'data-separator-notes' ),
243						attributes: getForwardedAttributes( section )
244					});
245
246				}
247
248			});
249
250			Promise.all( externalPromises ).then( resolve );
251
252		} );
253
254	}
255
256	function loadExternalMarkdown( section ) {
257
258		return new Promise( function( resolve, reject ) {
259
260			var xhr = new XMLHttpRequest(),
261				url = section.getAttribute( 'data-markdown' );
262
263			var datacharset = section.getAttribute( 'data-charset' );
264
265			// see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
266			if( datacharset != null && datacharset != '' ) {
267				xhr.overrideMimeType( 'text/html; charset=' + datacharset );
268			}
269
270			xhr.onreadystatechange = function( section, xhr ) {
271				if( xhr.readyState === 4 ) {
272					// file protocol yields status code 0 (useful for local debug, mobile applications etc.)
273					if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
274
275						resolve( xhr, url );
276
277					}
278					else {
279
280						reject( xhr, url );
281
282					}
283				}
284			}.bind( this, section, xhr );
285
286			xhr.open( 'GET', url, true );
287
288			try {
289				xhr.send();
290			}
291			catch ( e ) {
292				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 );
293				resolve( xhr, url );
294			}
295
296		} );
297
298	}
299
300	/**
301	 * Check if a node value has the attributes pattern.
302	 * If yes, extract it and add that value as one or several attributes
303	 * to the target element.
304	 *
305	 * You need Cache Killer on Chrome to see the effect on any FOM transformation
306	 * directly on refresh (F5)
307	 * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
308	 */
309	function addAttributeInElement( node, elementTarget, separator ) {
310
311		var mardownClassesInElementsRegex = new RegExp( separator, 'mg' );
312		var mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"]+?)\"|(data-[^\"= ]+?)(?=[\" ])", 'mg' );
313		var nodeValue = node.nodeValue;
314		var matches,
315			matchesClass;
316		if( matches = mardownClassesInElementsRegex.exec( nodeValue ) ) {
317
318			var classes = matches[1];
319			nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex );
320			node.nodeValue = nodeValue;
321			while( matchesClass = mardownClassRegex.exec( classes ) ) {
322				if( matchesClass[2] ) {
323					elementTarget.setAttribute( matchesClass[1], matchesClass[2] );
324				} else {
325					elementTarget.setAttribute( matchesClass[3], "" );
326				}
327			}
328			return true;
329		}
330		return false;
331	}
332
333	/**
334	 * Add attributes to the parent element of a text node,
335	 * or the element of an attribute node.
336	 */
337	function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) {
338
339		if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) {
340			var previousParentElement = element;
341			for( var i = 0; i < element.childNodes.length; i++ ) {
342				var childElement = element.childNodes[i];
343				if ( i > 0 ) {
344					var j = i - 1;
345					while ( j >= 0 ) {
346						var aPreviousChildElement = element.childNodes[j];
347						if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != "BR" ) {
348							previousParentElement = aPreviousChildElement;
349							break;
350						}
351						j = j - 1;
352					}
353				}
354				var parentSection = section;
355				if( childElement.nodeName ==  "section" ) {
356					parentSection = childElement ;
357					previousParentElement = childElement ;
358				}
359				if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) {
360					addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes );
361				}
362			}
363		}
364
365		if ( element.nodeType == Node.COMMENT_NODE ) {
366			if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) {
367				addAttributeInElement( element, section, separatorSectionAttributes );
368			}
369		}
370	}
371
372	/**
373	 * Converts any current data-markdown slides in the
374	 * DOM to HTML.
375	 */
376	function convertSlides() {
377
378		var sections = deck.getRevealElement().querySelectorAll( '[data-markdown]:not([data-markdown-parsed])');
379
380		[].slice.call( sections ).forEach( function( section ) {
381
382			section.setAttribute( 'data-markdown-parsed', true )
383
384			var notes = section.querySelector( 'aside.notes' );
385			var markdown = getMarkdownFromSlide( section );
386
387			section.innerHTML = marked( markdown );
388			addAttributes( 	section, section, null, section.getAttribute( 'data-element-attributes' ) ||
389							section.parentNode.getAttribute( 'data-element-attributes' ) ||
390							DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
391							section.getAttribute( 'data-attributes' ) ||
392							section.parentNode.getAttribute( 'data-attributes' ) ||
393							DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
394
395			// If there were notes, we need to re-add them after
396			// having overwritten the section's HTML
397			if( notes ) {
398				section.appendChild( notes );
399			}
400
401		} );
402
403		return Promise.resolve();
404
405	}
406
407	function escapeForHTML( input ) {
408
409	  return input.replace( /([&<>'"])/g, char => HTML_ESCAPE_MAP[char] );
410
411	}
412
413	return {
414		id: 'markdown',
415
416		/**
417		 * Starts processing and converting Markdown within the
418		 * current reveal.js deck.
419		 */
420		init: function( reveal ) {
421
422			deck = reveal;
423
424			let { renderer, animateLists, ...markedOptions } = deck.getConfig().markdown || {};
425
426			if( !renderer ) {
427				renderer = new marked.Renderer();
428
429				renderer.code = ( code, language ) => {
430
431					// Off by default
432					let lineNumbers = '';
433
434					// Users can opt in to show line numbers and highlight
435					// specific lines.
436					// ```javascript []        show line numbers
437					// ```javascript [1,4-8]   highlights lines 1 and 4-8
438					if( CODE_LINE_NUMBER_REGEX.test( language ) ) {
439						lineNumbers = language.match( CODE_LINE_NUMBER_REGEX )[1].trim();
440						lineNumbers = `data-line-numbers="${lineNumbers}"`;
441						language = language.replace( CODE_LINE_NUMBER_REGEX, '' ).trim();
442					}
443
444					// Escape before this gets injected into the DOM to
445					// avoid having the HTML parser alter our code before
446					// highlight.js is able to read it
447					code = escapeForHTML( code );
448
449					return `<pre><code ${lineNumbers} class="${language}">${code}</code></pre>`;
450				};
451			}
452
453			if( animateLists === true ) {
454				renderer.listitem = text => `<li class="fragment">${text}</li>`;
455			}
456
457			marked.setOptions( {
458				renderer,
459				...markedOptions
460			} );
461
462			return processSlides( deck.getRevealElement() ).then( convertSlides );
463
464		},
465
466		// TODO: Do these belong in the API?
467		processSlides: processSlides,
468		convertSlides: convertSlides,
469		slidify: slidify,
470		marked: marked
471	}
472
473};
474
475export default Plugin;