presentations

Presentations
Log | Files | Refs

markdown.js (13337B)


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