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