|
|
|
|
|
|
|
|
|
|
|
|
|
import { marked } from 'marked'; |
|
|
|
const DEFAULT_SLIDE_SEPARATOR = '\r?\n---\r?\n', |
|
DEFAULT_NOTES_SEPARATOR = 'notes?:', |
|
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$', |
|
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$'; |
|
|
|
const SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__'; |
|
|
|
const CODE_LINE_NUMBER_REGEX = /\[([\s\d,|-]*)\]/; |
|
|
|
const HTML_ESCAPE_MAP = { |
|
'&': '&', |
|
'<': '<', |
|
'>': '>', |
|
'"': '"', |
|
"'": ''' |
|
}; |
|
|
|
const Plugin = () => { |
|
|
|
|
|
let deck; |
|
|
|
|
|
|
|
|
|
|
|
function getMarkdownFromSlide( section ) { |
|
|
|
|
|
var template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' ); |
|
|
|
|
|
var text = ( template || section ).textContent; |
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue; |
|
|
|
if( value ) { |
|
result.push( name + '="' + value + '"' ); |
|
} |
|
else { |
|
result.push( name ); |
|
} |
|
} |
|
|
|
return result.join( ' ' ); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
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>'; |
|
} |
|
|
|
|
|
|
|
content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER ); |
|
|
|
return '<script type="text/template">' + content + '</script>'; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 = []; |
|
|
|
|
|
while( matches = separatorRegex.exec( markdown ) ) { |
|
var notes = null; |
|
|
|
|
|
isHorizontal = horizontalSeparatorRegex.test( matches[0] ); |
|
|
|
if( !isHorizontal && wasHorizontal ) { |
|
|
|
sectionStack.push( [] ); |
|
} |
|
|
|
|
|
content = markdown.substring( lastIndex, matches.index ); |
|
|
|
if( isHorizontal && wasHorizontal ) { |
|
|
|
sectionStack.push( content ); |
|
} |
|
else { |
|
|
|
sectionStack[sectionStack.length-1].push( content ); |
|
} |
|
|
|
lastIndex = separatorRegex.lastIndex; |
|
wasHorizontal = isHorizontal; |
|
} |
|
|
|
|
|
( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) ); |
|
|
|
var markdownSections = ''; |
|
|
|
|
|
for( var i = 0, len = sectionStack.length; i < len; i++ ) { |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function processSlides( scope ) { |
|
|
|
return new Promise( function( resolve ) { |
|
|
|
var externalPromises = []; |
|
|
|
[].slice.call( scope.querySelectorAll( 'section[data-markdown]:not([data-markdown-parsed])') ).forEach( function( section, i ) { |
|
|
|
if( section.getAttribute( 'data-markdown' ).length ) { |
|
|
|
externalPromises.push( loadExternalMarkdown( section ).then( |
|
|
|
|
|
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 ) |
|
}); |
|
}, |
|
|
|
|
|
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 { |
|
|
|
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 ) |
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
Promise.all( externalPromises ).then( resolve ); |
|
|
|
} ); |
|
|
|
} |
|
|
|
function loadExternalMarkdown( section ) { |
|
|
|
return new Promise( function( resolve, reject ) { |
|
|
|
var xhr = new XMLHttpRequest(), |
|
url = section.getAttribute( 'data-markdown' ); |
|
|
|
var datacharset = section.getAttribute( 'data-charset' ); |
|
|
|
|
|
if( datacharset != null && datacharset != '' ) { |
|
xhr.overrideMimeType( 'text/html; charset=' + datacharset ); |
|
} |
|
|
|
xhr.onreadystatechange = function( section, xhr ) { |
|
if( xhr.readyState === 4 ) { |
|
|
|
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 ) { |
|
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 ); |
|
resolve( xhr, url ); |
|
} |
|
|
|
} ); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addAttributeInElement( node, elementTarget, separator ) { |
|
|
|
var mardownClassesInElementsRegex = new RegExp( separator, 'mg' ); |
|
var mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"]+?)\"|(data-[^\"= ]+?)(?=[\" ])", 'mg' ); |
|
var nodeValue = node.nodeValue; |
|
var matches, |
|
matchesClass; |
|
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 ) ) { |
|
if( matchesClass[2] ) { |
|
elementTarget.setAttribute( matchesClass[1], matchesClass[2] ); |
|
} else { |
|
elementTarget.setAttribute( matchesClass[3], "" ); |
|
} |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) { |
|
|
|
if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) { |
|
var previousParentElement = element; |
|
for( var i = 0; i < element.childNodes.length; i++ ) { |
|
var childElement = element.childNodes[i]; |
|
if ( i > 0 ) { |
|
var j = i - 1; |
|
while ( j >= 0 ) { |
|
var aPreviousChildElement = element.childNodes[j]; |
|
if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != "BR" ) { |
|
previousParentElement = aPreviousChildElement; |
|
break; |
|
} |
|
j = j - 1; |
|
} |
|
} |
|
var 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 ); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function convertSlides() { |
|
|
|
var sections = deck.getRevealElement().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( notes ) { |
|
section.appendChild( notes ); |
|
} |
|
|
|
} ); |
|
|
|
return Promise.resolve(); |
|
|
|
} |
|
|
|
function escapeForHTML( input ) { |
|
|
|
return input.replace( /([&<>'"])/g, char => HTML_ESCAPE_MAP[char] ); |
|
|
|
} |
|
|
|
return { |
|
id: 'markdown', |
|
|
|
|
|
|
|
|
|
|
|
init: function( reveal ) { |
|
|
|
deck = reveal; |
|
|
|
let { renderer, animateLists, ...markedOptions } = deck.getConfig().markdown || {}; |
|
|
|
if( !renderer ) { |
|
renderer = new marked.Renderer(); |
|
|
|
renderer.code = ( code, language ) => { |
|
|
|
|
|
let lineNumbers = ''; |
|
|
|
|
|
|
|
|
|
|
|
if( CODE_LINE_NUMBER_REGEX.test( language ) ) { |
|
lineNumbers = language.match( CODE_LINE_NUMBER_REGEX )[1].trim(); |
|
lineNumbers = `data-line-numbers="${lineNumbers}"`; |
|
language = language.replace( CODE_LINE_NUMBER_REGEX, '' ).trim(); |
|
} |
|
|
|
|
|
|
|
|
|
code = escapeForHTML( code ); |
|
|
|
return `<pre><code ${lineNumbers} class="${language}">${code}</code></pre>`; |
|
}; |
|
} |
|
|
|
if( animateLists === true ) { |
|
renderer.listitem = text => `<li class="fragment">${text}</li>`; |
|
} |
|
|
|
marked.setOptions( { |
|
renderer, |
|
...markedOptions |
|
} ); |
|
|
|
return processSlides( deck.getRevealElement() ).then( convertSlides ); |
|
|
|
}, |
|
|
|
|
|
processSlides: processSlides, |
|
convertSlides: convertSlides, |
|
slidify: slidify, |
|
marked: marked |
|
} |
|
|
|
}; |
|
|
|
export default Plugin; |
|
|