|
|
@@ -0,0 +1,446 @@ |
|
|
|
/** |
|
|
|
* The reveal.js markdown plugin. Handles parsing of |
|
|
|
* markdown inside of presentations as well as loading |
|
|
|
* of external markdown documents. |
|
|
|
*/ |
|
|
|
(function( root, factory ) { |
|
|
|
if (typeof define === 'function' && define.amd) { |
|
|
|
root.marked = require( './marked' ); |
|
|
|
root.RevealMarkdown = factory( root.marked ); |
|
|
|
} else if( typeof exports === 'object' ) { |
|
|
|
module.exports = factory( require( './marked' ) ); |
|
|
|
} else { |
|
|
|
// Browser globals (root is window) |
|
|
|
root.RevealMarkdown = factory( root.marked ); |
|
|
|
} |
|
|
|
}( this, function( marked ) { |
|
|
|
|
|
|
|
var DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$', |
|
|
|
DEFAULT_NOTES_SEPARATOR = 'notes?:', |
|
|
|
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$', |
|
|
|
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$'; |
|
|
|
|
|
|
|
var SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__'; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieves the markdown contents of a slide section |
|
|
|
* element. Normalizes leading tabs/whitespace. |
|
|
|
*/ |
|
|
|
function getMarkdownFromSlide( section ) { |
|
|
|
|
|
|
|
// look for a <script> or <textarea data-template> wrapper |
|
|
|
var template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' ); |
|
|
|
|
|
|
|
// strip leading whitespace so it isn't evaluated as code |
|
|
|
var text = ( template || section ).textContent; |
|
|
|
|
|
|
|
// restore script end tags |
|
|
|
text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' ); |
|
|
|
|
|
|
|
var leadingWs = text.match( /^\n?(\s*)/ )[1].length, |
|
|
|
leadingTabs = text.match( /^\n?(\t*)/ )[1].length; |
|
|
|
|
|
|
|
if( leadingTabs > 0 ) { |
|
|
|
text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}','g'), '\n' ); |
|
|
|
} |
|
|
|
else if( leadingWs > 1 ) { |
|
|
|
text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' ); |
|
|
|
} |
|
|
|
|
|
|
|
return text; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Given a markdown slide section element, this will |
|
|
|
* return all arguments that aren't related to markdown |
|
|
|
* parsing. Used to forward any other user-defined arguments |
|
|
|
* to the output markdown slide. |
|
|
|
*/ |
|
|
|
function getForwardedAttributes( section ) { |
|
|
|
|
|
|
|
var attributes = section.attributes; |
|
|
|
var result = []; |
|
|
|
|
|
|
|
for( var i = 0, len = attributes.length; i < len; i++ ) { |
|
|
|
var name = attributes[i].name, |
|
|
|
value = attributes[i].value; |
|
|
|
|
|
|
|
// disregard attributes that are used for markdown loading/parsing |
|
|
|
if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue; |
|
|
|
|
|
|
|
if( value ) { |
|
|
|
result.push( name + '="' + value + '"' ); |
|
|
|
} |
|
|
|
else { |
|
|
|
result.push( name ); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return result.join( ' ' ); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Inspects the given options and fills out default |
|
|
|
* values for what's not defined. |
|
|
|
*/ |
|
|
|
function getSlidifyOptions( options ) { |
|
|
|
|
|
|
|
options = options || {}; |
|
|
|
options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR; |
|
|
|
options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR; |
|
|
|
options.attributes = options.attributes || ''; |
|
|
|
|
|
|
|
return options; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Helper function for constructing a markdown slide. |
|
|
|
*/ |
|
|
|
function createMarkdownSlide( content, options ) { |
|
|
|
|
|
|
|
options = getSlidifyOptions( options ); |
|
|
|
|
|
|
|
var notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) ); |
|
|
|
|
|
|
|
if( notesMatch.length === 2 ) { |
|
|
|
content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>'; |
|
|
|
} |
|
|
|
|
|
|
|
// prevent script end tags in the content from interfering |
|
|
|
// with parsing |
|
|
|
content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER ); |
|
|
|
|
|
|
|
return '<script type="text/template">' + content + '</script>'; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Parses a data string into multiple slides based |
|
|
|
* on the passed in separator arguments. |
|
|
|
*/ |
|
|
|
function slidify( markdown, options ) { |
|
|
|
|
|
|
|
options = getSlidifyOptions( options ); |
|
|
|
|
|
|
|
var separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ), |
|
|
|
horizontalSeparatorRegex = new RegExp( options.separator ); |
|
|
|
|
|
|
|
var matches, |
|
|
|
lastIndex = 0, |
|
|
|
isHorizontal, |
|
|
|
wasHorizontal = true, |
|
|
|
content, |
|
|
|
sectionStack = []; |
|
|
|
|
|
|
|
// iterate until all blocks between separators are stacked up |
|
|
|
while( matches = separatorRegex.exec( markdown ) ) { |
|
|
|
notes = null; |
|
|
|
|
|
|
|
// determine direction (horizontal by default) |
|
|
|
isHorizontal = horizontalSeparatorRegex.test( matches[0] ); |
|
|
|
|
|
|
|
if( !isHorizontal && wasHorizontal ) { |
|
|
|
// create vertical stack |
|
|
|
sectionStack.push( [] ); |
|
|
|
} |
|
|
|
|
|
|
|
// pluck slide content from markdown input |
|
|
|
content = markdown.substring( lastIndex, matches.index ); |
|
|
|
|
|
|
|
if( isHorizontal && wasHorizontal ) { |
|
|
|
// add to horizontal stack |
|
|
|
sectionStack.push( content ); |
|
|
|
} |
|
|
|
else { |
|
|
|
// add to vertical stack |
|
|
|
sectionStack[sectionStack.length-1].push( content ); |
|
|
|
} |
|
|
|
|
|
|
|
lastIndex = separatorRegex.lastIndex; |
|
|
|
wasHorizontal = isHorizontal; |
|
|
|
} |
|
|
|
|
|
|
|
// add the remaining slide |
|
|
|
( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) ); |
|
|
|
|
|
|
|
var markdownSections = ''; |
|
|
|
|
|
|
|
// flatten the hierarchical stack, and insert <section data-markdown> tags |
|
|
|
for( var i = 0, len = sectionStack.length; i < len; i++ ) { |
|
|
|
// vertical |
|
|
|
if( sectionStack[i] instanceof Array ) { |
|
|
|
markdownSections += '<section '+ options.attributes +'>'; |
|
|
|
|
|
|
|
sectionStack[i].forEach( function( child ) { |
|
|
|
markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>'; |
|
|
|
} ); |
|
|
|
|
|
|
|
markdownSections += '</section>'; |
|
|
|
} |
|
|
|
else { |
|
|
|
markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>'; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return markdownSections; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Parses any current data-markdown slides, splits |
|
|
|
* multi-slide markdown into separate sections and |
|
|
|
* handles loading of external markdown. |
|
|
|
*/ |
|
|
|
function processSlides() { |
|
|
|
|
|
|
|
return new Promise( function( resolve ) { |
|
|
|
|
|
|
|
var externalPromises = []; |
|
|
|
|
|
|
|
[].slice.call( document.querySelectorAll( '[data-markdown]') ).forEach( function( section, i ) { |
|
|
|
|
|
|
|
if( section.getAttribute( 'data-markdown' ).length ) { |
|
|
|
|
|
|
|
externalPromises.push( loadExternalMarkdown( section ).then( |
|
|
|
|
|
|
|
// Finished loading external file |
|
|
|
function( xhr, url ) { |
|
|
|
section.outerHTML = slidify( xhr.responseText, { |
|
|
|
separator: section.getAttribute( 'data-separator' ), |
|
|
|
verticalSeparator: section.getAttribute( 'data-separator-vertical' ), |
|
|
|
notesSeparator: section.getAttribute( 'data-separator-notes' ), |
|
|
|
attributes: getForwardedAttributes( section ) |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// Failed to load markdown |
|
|
|
function( xhr, url ) { |
|
|
|
section.outerHTML = '<section data-state="alert">' + |
|
|
|
'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + |
|
|
|
'Check your browser\'s JavaScript console for more details.' + |
|
|
|
'<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' + |
|
|
|
'</section>'; |
|
|
|
} |
|
|
|
|
|
|
|
) ); |
|
|
|
|
|
|
|
} |
|
|
|
else if( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) { |
|
|
|
|
|
|
|
section.outerHTML = slidify( getMarkdownFromSlide( section ), { |
|
|
|
separator: section.getAttribute( 'data-separator' ), |
|
|
|
verticalSeparator: section.getAttribute( 'data-separator-vertical' ), |
|
|
|
notesSeparator: section.getAttribute( 'data-separator-notes' ), |
|
|
|
attributes: getForwardedAttributes( section ) |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
else { |
|
|
|
section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) ); |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
Promise.all( externalPromises ).then( resolve ); |
|
|
|
|
|
|
|
} ); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
function loadExternalMarkdown( section ) { |
|
|
|
|
|
|
|
return new Promise( function( resolve, reject ) { |
|
|
|
|
|
|
|
var xhr = new XMLHttpRequest(), |
|
|
|
url = section.getAttribute( 'data-markdown' ); |
|
|
|
|
|
|
|
datacharset = section.getAttribute( 'data-charset' ); |
|
|
|
|
|
|
|
// see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes |
|
|
|
if( datacharset != null && datacharset != '' ) { |
|
|
|
xhr.overrideMimeType( 'text/html; charset=' + datacharset ); |
|
|
|
} |
|
|
|
|
|
|
|
xhr.onreadystatechange = function( section, xhr ) { |
|
|
|
if( xhr.readyState === 4 ) { |
|
|
|
// file protocol yields status code 0 (useful for local debug, mobile applications etc.) |
|
|
|
if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) { |
|
|
|
|
|
|
|
resolve( xhr, url ); |
|
|
|
|
|
|
|
} |
|
|
|
else { |
|
|
|
|
|
|
|
reject( xhr, url ); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}.bind( this, section, xhr ); |
|
|
|
|
|
|
|
xhr.open( 'GET', url, true ); |
|
|
|
|
|
|
|
try { |
|
|
|
xhr.send(); |
|
|
|
} |
|
|
|
catch ( e ) { |
|
|
|
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 ); |
|
|
|
resolve( xhr, url ); |
|
|
|
} |
|
|
|
|
|
|
|
} ); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Check if a node value has the attributes pattern. |
|
|
|
* If yes, extract it and add that value as one or several attributes |
|
|
|
* to the target element. |
|
|
|
* |
|
|
|
* You need Cache Killer on Chrome to see the effect on any FOM transformation |
|
|
|
* directly on refresh (F5) |
|
|
|
* http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277 |
|
|
|
*/ |
|
|
|
function addAttributeInElement( node, elementTarget, separator ) { |
|
|
|
|
|
|
|
var mardownClassesInElementsRegex = new RegExp( separator, 'mg' ); |
|
|
|
var mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"=]+?)\"", 'mg' ); |
|
|
|
var nodeValue = node.nodeValue; |
|
|
|
if( matches = mardownClassesInElementsRegex.exec( nodeValue ) ) { |
|
|
|
|
|
|
|
var classes = matches[1]; |
|
|
|
nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex ); |
|
|
|
node.nodeValue = nodeValue; |
|
|
|
while( matchesClass = mardownClassRegex.exec( classes ) ) { |
|
|
|
elementTarget.setAttribute( matchesClass[1], matchesClass[2] ); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Add attributes to the parent element of a text node, |
|
|
|
* or the element of an attribute node. |
|
|
|
*/ |
|
|
|
function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) { |
|
|
|
|
|
|
|
if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) { |
|
|
|
previousParentElement = element; |
|
|
|
for( var i = 0; i < element.childNodes.length; i++ ) { |
|
|
|
childElement = element.childNodes[i]; |
|
|
|
if ( i > 0 ) { |
|
|
|
j = i - 1; |
|
|
|
while ( j >= 0 ) { |
|
|
|
aPreviousChildElement = element.childNodes[j]; |
|
|
|
if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != "BR" ) { |
|
|
|
previousParentElement = aPreviousChildElement; |
|
|
|
break; |
|
|
|
} |
|
|
|
j = j - 1; |
|
|
|
} |
|
|
|
} |
|
|
|
parentSection = section; |
|
|
|
if( childElement.nodeName == "section" ) { |
|
|
|
parentSection = childElement ; |
|
|
|
previousParentElement = childElement ; |
|
|
|
} |
|
|
|
if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) { |
|
|
|
addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if ( element.nodeType == Node.COMMENT_NODE ) { |
|
|
|
if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) { |
|
|
|
addAttributeInElement( element, section, separatorSectionAttributes ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Converts any current data-markdown slides in the |
|
|
|
* DOM to HTML. |
|
|
|
*/ |
|
|
|
function convertSlides() { |
|
|
|
|
|
|
|
var sections = document.querySelectorAll( '[data-markdown]:not([data-markdown-parsed])'); |
|
|
|
|
|
|
|
[].slice.call( sections ).forEach( function( section ) { |
|
|
|
|
|
|
|
section.setAttribute( 'data-markdown-parsed', true ) |
|
|
|
|
|
|
|
var notes = section.querySelector( 'aside.notes' ); |
|
|
|
var markdown = getMarkdownFromSlide( section ); |
|
|
|
|
|
|
|
section.innerHTML = marked( markdown ); |
|
|
|
addAttributes( section, section, null, section.getAttribute( 'data-element-attributes' ) || |
|
|
|
section.parentNode.getAttribute( 'data-element-attributes' ) || |
|
|
|
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR, |
|
|
|
section.getAttribute( 'data-attributes' ) || |
|
|
|
section.parentNode.getAttribute( 'data-attributes' ) || |
|
|
|
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR); |
|
|
|
|
|
|
|
// If there were notes, we need to re-add them after |
|
|
|
// having overwritten the section's HTML |
|
|
|
if( notes ) { |
|
|
|
section.appendChild( notes ); |
|
|
|
} |
|
|
|
|
|
|
|
} ); |
|
|
|
|
|
|
|
return Promise.resolve(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// API |
|
|
|
var RevealMarkdown = { |
|
|
|
|
|
|
|
/** |
|
|
|
* Starts processing and converting Markdown within the |
|
|
|
* current reveal.js deck. |
|
|
|
* |
|
|
|
* @param {function} callback function to invoke once |
|
|
|
* we've finished loading and parsing Markdown |
|
|
|
*/ |
|
|
|
init: function( callback ) { |
|
|
|
|
|
|
|
if( typeof marked === 'undefined' ) { |
|
|
|
throw 'The reveal.js Markdown plugin requires marked to be loaded'; |
|
|
|
} |
|
|
|
|
|
|
|
if( typeof hljs !== 'undefined' ) { |
|
|
|
marked.setOptions({ |
|
|
|
highlight: function( code, lang ) { |
|
|
|
return hljs.highlightAuto( code, [lang] ).value; |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// marked can be configured via reveal.js config options |
|
|
|
var options = Reveal.getConfig().markdown; |
|
|
|
if( options ) { |
|
|
|
marked.setOptions( options ); |
|
|
|
} |
|
|
|
|
|
|
|
return processSlides().then( convertSlides ); |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// TODO: Do these belong in the API? |
|
|
|
processSlides: processSlides, |
|
|
|
convertSlides: convertSlides, |
|
|
|
slidify: slidify |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
// Register our plugin so that reveal.js will call our |
|
|
|
// plugin 'init' method as part of the initialization |
|
|
|
Reveal.registerPlugin( 'markdown', RevealMarkdown ); |
|
|
|
|
|
|
|
return RevealMarkdown; |
|
|
|
|
|
|
|
})); |