/*! * The reveal.js markdown plugin. Handles parsing of * markdown inside of presentations as well as loading * of external markdown documents. */ 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 = () => { // The reveal.js instance this plugin is attached to let deck; /** * 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"), function (m, p1) { return "\n" + p1; } ); } else if (leadingWs > 1) { text = text.replace( new RegExp("\\n? {" + leadingWs + "}(.*)", "g"), function (m, p1) { return "\n" + p1; } ); } 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))) { var 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(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( // 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 = '
' + "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.

" + "
"; } ) ); } 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"); // 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) { 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); } }); } /** * 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( '([^"= ]+?)="([^"]+?)"|(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; } /** * 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 ) { 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); } } } /** * Converts any current data-markdown slides in the * DOM to HTML. */ 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 there were notes, we need to re-add them after // having overwritten the section's HTML if (notes) { section.appendChild(notes); } }); return Promise.resolve(); } function escapeForHTML(input) { return input.replace(/([&<>'"])/g, (char) => HTML_ESCAPE_MAP[char]); } return { id: "markdown", /** * Starts processing and converting Markdown within the * current reveal.js deck. */ init: function (reveal) { deck = reveal; let { renderer, animateLists, ...markedOptions } = deck.getConfig().markdown || {}; if (!renderer) { renderer = new marked.Renderer(); renderer.code = (code, language) => { // Off by default let lineNumbers = ""; // Users can opt in to show line numbers and highlight // specific lines. // ```javascript [] show line numbers // ```javascript [1,4-8] highlights lines 1 and 4-8 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(); } // Escape before this gets injected into the DOM to // avoid having the HTML parser alter our code before // highlight.js is able to read it code = escapeForHTML(code); return `
${code}
`; }; } if (animateLists === true) { renderer.listitem = (text) => `
  • ${text}
  • `; } marked.setOptions({ renderer, ...markedOptions, }); return processSlides(deck.getRevealElement()).then(convertSlides); }, // TODO: Do these belong in the API? processSlides: processSlides, convertSlides: convertSlides, slidify: slidify, marked: marked, }; }; export default Plugin;