/* eslint-env browser, jquery */ import { preventXSS, escapeAttrValue } from './render' import { md } from './extra' /** * 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 exports === 'object') { module.exports = factory() } else { // Browser globals (root is window) root.RevealMarkdown = factory() root.RevealMarkdown.initialize() } }(this, function () { var DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$' var DEFAULT_NOTES_SEPARATOR = '^note:' var DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\.element\\s*?(.+?)$' var 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) { var 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'), '') var leadingWs = text.match(/^\n?(\s*)/)[1].length var 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 var 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') var horizontalSeparatorRegex = new RegExp(options.separator) var matches var lastIndex = 0 var isHorizontal var wasHorizontal = true var content var sectionStack = [] // iterate until all blocks between separators are stacked up while ((matches = separatorRegex.exec(markdown)) !== 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]') var section for (var i = 0, len = sections.length; i < len; i++) { section = sections[i] if (section.getAttribute('data-markdown').length) { var xhr = new XMLHttpRequest() var 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 () { 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 var matches var 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))) { var name = matchesClass[1] var value = matchesClass[2] if (name.substr(0, 5) === 'data-' || window.whiteListAttr.indexOf(name) !== -1) { elementTarget.setAttribute(name, escapeAttrValue(value)) } } 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) { let 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 = 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) markdown = markdown.replace(/</g, '<').replace(/>/g, '>') var rendered = md.render(markdown) rendered = preventXSS(rendered) var result = window.postProcess(rendered) section.innerHTML = result[0].outerHTML 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 () { processSlides() convertSlides() }, // TODO: Do these belong in the API? processSlides: processSlides, convertSlides: convertSlides, slidify: slidify } }))