/** * 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); root.RevealMarkdown.initialize(); } else if (typeof exports === "object") { module.exports = factory(require("./marked")); } else { // Browser globals (root is window) root.RevealMarkdown = factory(root.marked); root.RevealMarkdown.initialize(); } })(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 "); 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] + '"; } // prevent script end tags in the content from interfering // with parsing content = content.replace(/<\/script>/g, SCRIPT_END_PLACEHOLDER); return '"; } /** * 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
tags for (var i = 0, len = sectionStack.length; i < len; i++) { // vertical if (sectionStack[i] instanceof Array) { markdownSections += "
"; sectionStack[i].forEach(function(child) { markdownSections += "
" + createMarkdownSlide(child, options) + "
"; }); markdownSections += "
"; } else { markdownSections += "
" + createMarkdownSlide(sectionStack[i], options) + "
"; } } return markdownSections; } /** * Parses any current data-markdown slides, splits * multi-slide markdown into separate sections and * handles loading of external markdown. */ function processSlides() { var sections = document.querySelectorAll("[data-markdown]"), section; for (var i = 0, len = sections.length; i < len; i++) { section = sections[i]; if (section.getAttribute("data-markdown").length) { 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() { 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) { 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) }); } else { section.outerHTML = '
' + "ERROR: The attempt to fetch " + url + " failed with HTTP status " + xhr.status + "." + "Check your browser's JavaScript console for more details." + "

Remember that you need to serve the presentation HTML from a HTTP server.

" + "
"; } } }; xhr.open("GET", url, false); 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 ); } } 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)); } } } /** * Check if a node value has the attributes pattern. * If yes, extract it and add that value as one or several attributes * the the terget 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]"); for (var i = 0, len = sections.length; i < len; i++) { var section = sections[i]; // Only parse the same slide once if (!section.getAttribute("data-markdown-parsed")) { 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); } } } } // API return { initialize: function() { 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; } }); } var options = Reveal.getConfig().markdown; if (options) { marked.setOptions(options); } processSlides(); convertSlides(); }, // TODO: Do these belong in the API? processSlides: processSlides, convertSlides: convertSlides, slidify: slidify }; });