presentations/reveal/plugin/markdown/markdown.js

444 lines
14 KiB
JavaScript
Raw Normal View History

2018-07-29 22:20:49 +02:00
/**
* 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 <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() {
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 =
'<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>";
}
}
};
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
};
});