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}));
447