diff --git a/js/core/markdown.js b/js/core/markdown.js new file mode 100644 index 0000000000..448abe500b --- /dev/null +++ b/js/core/markdown.js @@ -0,0 +1,208 @@ +// Module core/markdown +// Handles the optional markdown processing. +// +// Markdown support is optional. It is enabled by setting the `format` +// property of the configuration object to "markdown." +// +// We use marked for parsing Markkdown. +// +// Note that the content of SECTION elements, and P elements with a +// class name of "note" or "issue" is also parsed. +// +// The HTML created by the Markdown parser is turned into a nested +// structure of SECTION elements, following the strucutre given by +// the headings. For example, the following markup: +// +// Title +// ----- +// +// ### Subtitle ### +// +// Here's some text. +// +// ### Another subtitle ### +// +// More text. +// +// will be transformed into: +// +//
+//

Title

+//
+//

Subtitle

+//

Here's some text.

+//
+//
+//

Another subtitle

+//

More text.

+//
+//
+ +define( + ['core/marked'], + function (markdown) { + marked.setOptions({ + gfm: false, + pedantic: false, + sanitize: false + }); + + return { + toHTML: function(text) { + // As markdown is pulled from HTML > is already escaped, and + // thus blockquotes aren't picked up by the parser. This fixes + // it. + text = text.replace(/>/g, '>'); + text = this.removeLeftPadding(text); + return marked(text); + }, + + removeLeftPadding: function(text) { + // Handles markdown content being nested + // inside elements with soft tabs. E.g.: + //
+ // This is a title + // --------------- + // + // And this more text. + //
+ //

This is a title

+ //

And this more text.

+ // + //
This is a title
+                // ---------------
+                // 
+                // And this more text.
+ // current) { + min = current + } + } + + var re = new RegExp("\n[ ]{0," + min + "}", "g"); + text = text.replace(re, '\n'); + } + return text; + }, + + processBody: function(doc) { + var fragment = doc.createDocumentFragment() + , div = doc.createElement('div') + , node + ; + + div.innerHTML = this.toHTML(doc.body.innerHTML); + while (node = div.firstChild) { + fragment.appendChild(node); + } + return fragment; + }, + + processSections: function(doc) { + var self = this; + $('section', doc).each(function() { + this.innerHTML = self.toHTML(this.innerHTML); + }); + }, + + processIssuesAndNotes: function(doc) { + var div = doc.createElement('div'); + var self = this; + $('.issue, .note', doc).each(function() { + div.innerHTML = self.toHTML(this.innerHTML); + this.innerHTML = ''; + var node = div.firstChild; + while (node.firstChild) { + this.appendChild(node.firstChild); + } + }); + }, + + structure: function(fragment, doc) { + var output = doc.createDocumentFragment() + , current = output + , stack = [output] + , node + , tagName + ; + + function newSection(node, position) { + var section = doc.createElement('section'); + section.appendChild(node); + findParent(position).appendChild(section); + stack[position] = section; + current = section; + } + + function findParent(position) { + while (1) { + position-- + parent = stack[position]; + if (parent) return parent; + } + } + + while (node = fragment.firstChild) { + if (node.nodeType !== 1) { + fragment.removeChild(node); + continue; + } + tagName = node.tagName.toLowerCase(); + switch (tagName) { + case 'h1': + newSection(node, 1); + break; + case 'h2': + newSection(node, 2); + break; + case 'h3': + newSection(node, 3); + break; + case 'h4': + newSection(node, 4); + break; + case 'h5': + newSection(node, 5); + break; + case 'h6': + newSection(node, 6); + break; + default: + current.appendChild(node); + } + } + + return output; + }, + + run: function (conf, doc, cb, msg) { + msg.pub("start", "core/markdown"); + if (conf.format === 'markdown') { + this.processIssuesAndNotes(doc); + this.processSections(doc); + fragment = this.structure(this.processBody(doc), doc); + doc.body.innerHTML = ''; + doc.body.appendChild(fragment) + } + msg.pub("end", "core/markdown"); + cb(); + } + }; + } +); diff --git a/js/core/marked.js b/js/core/marked.js new file mode 100644 index 0000000000..62a5eb9993 --- /dev/null +++ b/js/core/marked.js @@ -0,0 +1,781 @@ +/** + * marked - A markdown parser (https://github.com/chjj/marked) + * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) + */ + +;(function() { + +/** + * Block-Level Grammar + */ + +var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^( *[-*_]){3,} *(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, + lheading: /^([^\n]+)\n *(=|-){3,} *\n*/, + blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/, + list: /^( *)(bull) [^\0]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, + def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, + paragraph: /^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/, + text: /^[^\n]+/ +}; + +block.bullet = /(?:[*+-]|\d+\.)/; +block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; +block.item = replace(block.item, 'gm') + (/bull/g, block.bullet) + (); + +block.list = replace(block.list) + (/bull/g, block.bullet) + ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/) + (); + +block.html = replace(block.html) + ('comment', //) + ('closed', /<(tag)[^\0]+?<\/\1>/) + ('closing', /])*?>/) + (/tag/g, tag()) + (); + +block.paragraph = replace(block.paragraph) + ('hr', block.hr) + ('heading', block.heading) + ('lheading', block.lheading) + ('blockquote', block.blockquote) + ('tag', '<' + tag()) + ('def', block.def) + (); + +block.normal = { + fences: block.fences, + paragraph: block.paragraph +}; + +block.gfm = { + fences: /^ *(```|~~~) *(\w+)? *\n([^\0]+?)\s*\1 *(?:\n+|$)/, + paragraph: /^/ +}; + +block.gfm.paragraph = replace(block.paragraph) + ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|') + (); + +/** + * Block Lexer + */ + +block.lexer = function(src) { + var tokens = []; + + tokens.links = {}; + + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' '); + + return block.token(src, tokens, true); +}; + +block.token = function(src, tokens, top) { + var src = src.replace(/^ +$/gm, '') + , next + , loose + , cap + , item + , space + , i + , l; + + while (src) { + // newline + if (cap = block.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = block.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + tokens.push({ + type: 'code', + text: !options.pedantic + ? cap.replace(/\n+$/, '') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = block.fences.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'code', + lang: cap[2], + text: cap[3] + }); + continue; + } + + // heading + if (cap = block.heading.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // lheading + if (cap = block.lheading.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // hr + if (cap = block.hr.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = block.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + tokens.push({ + type: 'blockquote_start' + }); + + cap = cap[0].replace(/^ *> ?/gm, ''); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + block.token(cap, tokens, top); + + tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = block.list.exec(src)) { + src = src.substring(cap[0].length); + + tokens.push({ + type: 'list_start', + ordered: isFinite(cap[2]) + }); + + // Get each top-level item. + cap = cap[0].match(block.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item[item.length-1] === '\n'; + if (!loose) loose = next; + } + + tokens.push({ + type: loose + ? 'loose_item_start' + : 'list_item_start' + }); + + // Recurse. + block.token(item, tokens); + + tokens.push({ + type: 'list_item_end' + }); + } + + tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = block.html.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: options.sanitize + ? 'paragraph' + : 'html', + pre: cap[1] === 'pre', + text: cap[0] + }); + continue; + } + + // def + if (top && (cap = block.def.exec(src))) { + src = src.substring(cap[0].length); + tokens.links[cap[1].toLowerCase()] = { + href: cap[2], + title: cap[3] + }; + continue; + } + + // top-level paragraph + if (top && (cap = block.paragraph.exec(src))) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'paragraph', + text: cap[0] + }); + continue; + } + + // text + if (cap = block.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + } + + return tokens; +}; + +/** + * Inline Processing + */ + +var inline = { + escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, + autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, + url: noop, + tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, + link: /^!?\[(inside)\]\(href\)/, + reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, + nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, + strong: /^__([^\0]+?)__(?!_)|^\*\*([^\0]+?)\*\*(?!\*)/, + em: /^\b_((?:__|[^\0])+?)_\b|^\*((?:\*\*|[^\0])+?)\*(?!\*)/, + code: /^(`+)([^\0]*?[^`])\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + text: /^[^\0]+?(?=[\\?(?:\s+['"]([^\0]*?)['"])?\s*/; + +inline.link = replace(inline.link) + ('inside', inline._linkInside) + ('href', inline._linkHref) + (); + +inline.reflink = replace(inline.reflink) + ('inside', inline._linkInside) + (); + +inline.normal = { + url: inline.url, + strong: inline.strong, + em: inline.em, + text: inline.text +}; + +inline.pedantic = { + strong: /^__(?=\S)([^\0]*?\S)__(?!_)|^\*\*(?=\S)([^\0]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([^\0]*?\S)_(?!_)|^\*(?=\S)([^\0]*?\S)\*(?!\*)/ +}; + +inline.gfm = { + url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/, + text: /^[^\0]+?(?=[\\' + + text + + ''; + continue; + } + + // url (gfm) + if (cap = inline.url.exec(src)) { + src = src.substring(cap[0].length); + text = escape(cap[1]); + href = text; + out += '' + + text + + ''; + continue; + } + + // tag + if (cap = inline.tag.exec(src)) { + src = src.substring(cap[0].length); + out += options.sanitize + ? escape(cap[0]) + : cap[0]; + continue; + } + + // link + if (cap = inline.link.exec(src)) { + src = src.substring(cap[0].length); + out += outputLink(cap, { + href: cap[2], + title: cap[3] + }); + continue; + } + + // reflink, nolink + if ((cap = inline.reflink.exec(src)) + || (cap = inline.nolink.exec(src))) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0][0]; + src = cap[0].substring(1) + src; + continue; + } + out += outputLink(cap, link); + continue; + } + + // strong + if (cap = inline.strong.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + inline.lexer(cap[2] || cap[1]) + + ''; + continue; + } + + // em + if (cap = inline.em.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + inline.lexer(cap[2] || cap[1]) + + ''; + continue; + } + + // code + if (cap = inline.code.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + escape(cap[2], true) + + ''; + continue; + } + + // br + if (cap = inline.br.exec(src)) { + src = src.substring(cap[0].length); + out += '
'; + continue; + } + + // text + if (cap = inline.text.exec(src)) { + src = src.substring(cap[0].length); + out += escape(cap[0]); + continue; + } + } + + return out; +}; + +function outputLink(cap, link) { + if (cap[0][0] !== '!') { + return '' + + inline.lexer(cap[1]) + + ''; + } else { + return ''
+      + escape(cap[1])
+      + ''; + } +} + +/** + * Parsing + */ + +var tokens + , token; + +function next() { + return token = tokens.pop(); +} + +function tok() { + switch (token.type) { + case 'space': { + return ''; + } + case 'hr': { + return '
\n'; + } + case 'heading': { + return '' + + inline.lexer(token.text) + + '\n'; + } + case 'code': { + if (options.highlight) { + token.code = options.highlight(token.text, token.lang); + if (token.code != null && token.code !== token.text) { + token.escaped = true; + token.text = token.code; + } + } + + if (!token.escaped) { + token.text = escape(token.text, true); + } + + return '
'
+        + token.text
+        + '
\n'; + } + case 'blockquote_start': { + var body = ''; + + while (next().type !== 'blockquote_end') { + body += tok(); + } + + return '
\n' + + body + + '
\n'; + } + case 'list_start': { + var type = token.ordered ? 'ol' : 'ul' + , body = ''; + + while (next().type !== 'list_end') { + body += tok(); + } + + return '<' + + type + + '>\n' + + body + + '\n'; + } + case 'list_item_start': { + var body = ''; + + while (next().type !== 'list_item_end') { + body += token.type === 'text' + ? parseText() + : tok(); + } + + return '
  • ' + + body + + '
  • \n'; + } + case 'loose_item_start': { + var body = ''; + + while (next().type !== 'list_item_end') { + body += tok(); + } + + return '
  • ' + + body + + '
  • \n'; + } + case 'html': { + return !token.pre && !options.pedantic + ? inline.lexer(token.text) + : token.text; + } + case 'paragraph': { + return '

    ' + + inline.lexer(token.text) + + '

    \n'; + } + case 'text': { + return '

    ' + + parseText() + + '

    \n'; + } + } +} + +function parseText() { + var body = token.text + , top; + + while ((top = tokens[tokens.length-1]) + && top.type === 'text') { + body += '\n' + next().text; + } + + return inline.lexer(body); +} + +function parse(src) { + tokens = src.reverse(); + + var out = ''; + while (next()) { + out += tok(); + } + + tokens = null; + token = null; + + return out; +} + +/** + * Helpers + */ + +function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function mangle(text) { + var out = '' + , l = text.length + , i = 0 + , ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +} + +function tag() { + var tag = '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b'; + + return tag; +} + +function replace(regex, opt) { + regex = regex.source; + opt = opt || ''; + return function self(name, val) { + if (!name) return new RegExp(regex, opt); + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, '$1'); + regex = regex.replace(name, val); + return self; + }; +} + +function noop() {} +noop.exec = noop; + +/** + * Marked + */ + +function marked(src, opt) { + setOptions(opt); + return parse(block.lexer(src)); +} + +/** + * Options + */ + +var options + , defaults; + +function setOptions(opt) { + if (!opt) opt = defaults; + if (options === opt) return; + options = opt; + + if (options.gfm) { + block.fences = block.gfm.fences; + block.paragraph = block.gfm.paragraph; + inline.text = inline.gfm.text; + inline.url = inline.gfm.url; + } else { + block.fences = block.normal.fences; + block.paragraph = block.normal.paragraph; + inline.text = inline.normal.text; + inline.url = inline.normal.url; + } + + if (options.pedantic) { + inline.em = inline.pedantic.em; + inline.strong = inline.pedantic.strong; + } else { + inline.em = inline.normal.em; + inline.strong = inline.normal.strong; + } +} + +marked.options = +marked.setOptions = function(opt) { + defaults = opt; + setOptions(opt); + return marked; +}; + +marked.setOptions({ + gfm: true, + pedantic: false, + sanitize: false, + highlight: null +}); + +/** + * Expose + */ + +marked.parser = function(src, opt) { + setOptions(opt); + return parse(src); +}; + +marked.lexer = function(src, opt) { + setOptions(opt); + return block.lexer(src); +}; + +marked.parse = marked; + +if (typeof module !== 'undefined') { + module.exports = marked; +} else { + this.marked = marked; +} + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}()); \ No newline at end of file diff --git a/js/profile-w3c-common.js b/js/profile-w3c-common.js index 41a3f19ed7..56d9dab7f0 100644 --- a/js/profile-w3c-common.js +++ b/js/profile-w3c-common.js @@ -4,6 +4,7 @@ define([ , "core/base-runner" , "core/override-configuration" , "core/default-root-attr" + , "core/markdown" , "core/style" , "w3c/style" , "w3c/headers"