diff options
Diffstat (limited to 'presentation/plugin/markdown/markdown.js')
-rwxr-xr-x | presentation/plugin/markdown/markdown.js | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/presentation/plugin/markdown/markdown.js b/presentation/plugin/markdown/markdown.js new file mode 100755 index 0000000..c800bce --- /dev/null +++ b/presentation/plugin/markdown/markdown.js @@ -0,0 +1,436 @@ +/** + * 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; + +})); |