DisTract-0.2.5: html/lib/markdown.js
/*
* markdown.js
* - an javascript implementation of the "markdown" markup syntax
*
* Copyright (c) 2006, 2007, Matthew Sackman (matthew@wellquite.org)
*
* DisTract is freely distributable under the terms of a 3-Clause
* BSD-style license. For details, see the DisTract web site:
* http://distract.wellquite.org/
*
*/
function markdownToHTML(text, pages) {
var formatter = buildFormatter();
formatter.knownPages = pages;
var result = formatter.format(text);
result = doRefLinksSubstitutions(result, formatter.linkRefs);
result = doRefImgsSubstitutions(result, formatter.imgRefs);
return result;
}
function doRefLinksSubstitutions(text, links) {
var swap = function(linkObj, _idx, _ary) {
var reString = '(?:\\[(' + linkObj.id + ')\\][ \\t]*\\[\\])|(?:\\[([^\\]]+)\\][ \\t]*\\[' + linkObj.id + '\\])';
var re = new RegExp(reString, 'gi');
text = text.replace(re,
function(_matched, id1, linkText, _str, _offset) {
var linkText = linkText ? linkText : id1;
var title = linkObj.title ? ' title="' + linkObj.title + '"' : '';
return '<a href="' + linkObj.url + '"' + title + '>' + linkText + '</a>';
}
);
};
links.forEach(swap);
return text;
}
function doRefImgsSubstitutions(text, imgs) {
var swap = function(linkObj, _idx, _ary) {
var reString = '!(?:(?:\\[(' + linkObj.id + ')\\][ \\t]*\\[\\])|(?:\\[([^\\]]+)\\][ \\t]*\\[' + linkObj.id + '\\]))';
var re = new RegExp(reString, 'gi');
text = text.replace(re,
function(_matched, id1, altText, _str, _offset) {
var altText = altText ? altText : id1;
var title = linkObj.title ? ' title="' + linkObj.title + '"' : '';
return '<img src="' + linkObj.url + '"' + title + ' alt="' + altText + '" />';
}
);
};
imgs.forEach(swap);
return text;
}
function buildFormatterClone(formatter) {
var formatterClone = buildFormatter();
formatterClone.wrapRemainingInParaOrig = formatter.wrapRemainingInParaOrig;
formatterClone.knownPages = formatter.knownPages;
formatterClone.linkRefs = formatter.linkRefs;
formatterClone.imgRefs = formatter.imgRefs;
return formatterClone;
}
function buildFormatter() {
var formatter = new Object();
formatter.wrapRemainingInParaOrig = true;
formatter.linkRefs = new Array();
formatter.imgRefs = new Array();
formatter.paraSplitters = new Array();
formatter.paraSplitters.push(getSimpleParaSplitter());
formatter.blockFormatters = new Array();
formatter.blockFormatters.push(getBlockHeadersUnderline());
formatter.blockFormatters.push(getBlockHeadersHash());
formatter.blockFormatters.push(getBlockHR());
formatter.blockFormatters.push(getBlockCoder());
formatter.blockFormatters.push(getBlockQuoter());
formatter.blockFormatters.push(getBlockLister());
formatter.spanFormatters = new Array();
formatter.spanFormatters.push(getSpanReferenceSlurper());
formatter.spanFormatters.push(getSpanCode());
formatter.spanFormatters.push(getSpanWikiLinker());
formatter.spanFormatters.push(getSpanImager());
formatter.spanFormatters.push(getSpanLinker());
formatter.spanFormatters.push(getSpanBR());
formatter.spanFormatters.push(getSpanBoldItalic());
formatter.spanFormatters.push(getSpanAutoLinks());
formatter.invokeNextBlockFormatter = getNextBlockFun(formatter);
formatter.invokeNextSpanFormatter = getNextSpanFun(formatter);
formatter.format = getFormatFun(formatter);
return formatter;
}
function encodeAmpsAngles(text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function getNextBlockFun(formatter) {
return function() {
if (1 + formatter.blockIdx >= formatter.blockFormatters.length) {
formatter.invokeNextSpanFormatter();
} else {
formatter.blockIdx++;
var formatterFun = formatter.blockFormatters[formatter.blockIdx];
formatterFun(formatter);
}
};
}
function getNextSpanFun(formatter) {
return function() {
if (1 + formatter.spanIdx >= formatter.spanFormatters.length) {
return;
} else {
formatter.spanIdx++;
var formatterFun = formatter.spanFormatters[formatter.spanIdx];
formatterFun(formatter);
}
};
}
function getFormatFun(formatter) {
return function(text) {
if (! text) {
return '';
}
formatter.wrapRemainingInPara = formatter.wrapRemainingInParaOrig;
formatter.remaining = text;
var paraFun = formatter.paraSplitters[formatter.paraSplitters.length - 1];
paraFun(formatter);
formatter.unitDone = new Array();
formatter.blockIdx = -1;
formatter.spanIdx = -1;
formatter.invokeNextBlockFormatter();
if (formatter.unit) {
if (formatter.wrapRemainingInPara) {
formatter.unitDone.push('<p>' + formatter.unit + '</p>');
} else {
formatter.unitDone.push(formatter.unit);
}
formatter.unit = '';
}
var result = formatter.unitDone.join('\n');
if (formatter.remaining) {
result += '\n' + formatter.format(formatter.remaining);
}
return result;
};
}
function getSimpleParaSplitter() {
return function(state) {
var text = state.remaining;
if (undefined != text && null != text) {
text = text.replace(/^([ \t]*\n)+/g, '');
var matches = text.match(/^((?:.+\n)*.+)(?:$|(?:(?:\n)(?:[ \t]*\n)+))((?:.|\n)*)/);
if (matches) {
state.unit = matches[1];
state.remaining = matches[2] ? matches[2] : '';
} else {
state.unit = text;
state.remaining = '';
}
} else {
alert("Unable to split text into paragraphs:\n'" + text + "'");
}
return state;
};
}
function getBlockQuoter() {
return function(state) {
if (/^>[ \t]?/.test(state.unit)) {
var inner = state.unit.replace(/^>[ \t]?/mg, '');
var formatter = buildFormatterClone(state);
formatter.wrapRemainingInParaOrig = true;
state.unitDone.push('<blockquote>' + formatter.format(inner) + '</blockquote>');
state.unit = '';
} else {
state.invokeNextBlockFormatter();
}
};
}
function getBlockCoder() {
return function(state) {
if (/^(?:(?: )|\t)/.test(state.unit)) {
var matches = state.unit.match(/^((?:(?:(?: )|\t)(?:.*)(?:\n(?=( )|\t))?)+)((?:.|\n)*)$/);
if (matches) {
var code = matches[1].replace(/^(?:(?: )|\t)/mg, '');
state.unitDone.push('<pre><code>' + encodeAmpsAngles(code) + '</pre></code>');
var formatter = buildFormatterClone(state);
state.unitDone.push(formatter.format(matches[2]));
state.unit = '';
} else {
alert('Error on code block parser: ' + state.unit);
}
}
state.invokeNextBlockFormatter();
};
}
function getBlockLister() {
return function(state) {
var testerRe = /^(?:(?:\d+\.)|(?:[\+\*\-]))(?: |\t)+/;
if (state.inList) {
testerRe = /^(?:(?:\d+\.)|(?:[\+\*\-]))(?: |\t)+/m;
}
if (testerRe.test(state.unit)) {
var innerFormatter = buildFormatterClone(state);
innerFormatter.inList = false;
state.unit.replace(testerRe,
function(all, offset, _str) {
var pre = state.unit.substr(0, offset);
if (pre) {
state.unitDone.push(innerFormatter.format(pre));
state.unit = state.unit.substr(offset);
}
}
);
innerFormatter.inList = true;
var listEnd = '';
var listChar = '';
if (/^(?:\d+\.)/.test(state.unit)) {
state.unitDone.push('<ol>');
listEnd = '</ol>';
testerRe = /^(?:\d+\.)(?: |\t)+/;
if (state.inList) {
testerRe = /^(?:\d+\.)(?: |\t)+/m;
}
listChar = "\\d+\\.";
} else {
state.unitDone.push('<ul>');
listEnd = '</ul>';
var char = /^(.)/.exec(state.unit)[0];
testerRe = new RegExp("^(?:[" + char + "])(?: |\\t)+", '');
if (state.inList) {
testerRe = new RegExp("^(?:[" + char + "])(?: |\\t)+", 'm');
}
listChar = "[" + char + "]";
}
var paraSplitter = getSimpleParaSplitter();
var listElems = new Array();
var innerParaRe = /^(?:(?: )|\t)/;
var nextParaObject = new Object();
nextParaObject.unit = state.unit;
nextParaObject.remaining = state.remaining;
do {
var elem = nextParaObject.unit;
paraSplitter(nextParaObject);
while (innerParaRe.test(nextParaObject.unit)) {
elem += '\n\n' + nextParaObject.unit;
paraSplitter(nextParaObject);
}
listElems.push(elem);
} while (testerRe.test(nextParaObject.unit));
state.unit = '';
var extra = nextParaObject.unit ? nextParaObject.unit + '\n\n' : '';
state.remaining = extra + nextParaObject.remaining;
var formatFunction = function(e, _idx, _ary) {
innerFormatter.wrapRemainingInParaOrig = /^$\n[ \t]*\S+/m.test(e);
state.unitDone.push('<li>' + innerFormatter.format(e) + '</li>');
};
for (var idx = 0; idx < listElems.length; ++idx) {
var allListItems = new Array();
var leftOver = explodeListPara(listChar, listElems[idx], allListItems);
if (idx + 1 < listElems.length) {
// there's another para so the last list in this para is a p
var lastItem = allListItems.pop();
allListItems.forEach(formatFunction);
innerFormatter.wrapRemainingInParaOrig = true;
state.unitDone.push('<li>' + innerFormatter.format(lastItem) + '</li>');
} else {
// we're on the last para
if (listElems.length == 1) {
// if only para then all not p
allListItems.forEach(formatFunction);
} else {
// if more than 1 elem then first is p, rest aren't else p
var firstItem = allListItems.shift();
innerFormatter.wrapRemainingInParaOrig = true;
state.unitDone.push('<li>' + innerFormatter.format(firstItem) + '</li>');
allListItems.forEach(formatFunction);
}
}
if (leftOver) {
var remaining = leftOver;
if (idx + 1 < listElems.length) {
remaining = remaining + "\n\n" + (listElems.slice(idx + 1).join("\n\n"));
idx = listElems.length;
}
state.remaining = remaining + (state.remaining ? "\n\n" + state.remaining : '');
}
}
state.unitDone.push(listEnd);
} else {
state.invokeNextBlockFormatter();
}
};
}
function explodeListPara(char, text, ary) {
var re = new RegExp("^(?:[ \\t]*\\n*)(?:(?:" + char + ")[ \\t]+)((?:.+|\\n(?!((\\d+\\.)|([\\+\\*\\-]))))*)", '');
var simpleItem = null;
while (simpleItem = re.exec(text)) {
var item = simpleItem[1].replace(/^(?:(?: )|\t)/gm, '');
ary.push(item);
text = text.substr(simpleItem[0].length);
}
return text;
}
function getBlockHeadersHash() {
return function(state) {
if (/^#{1,6}( |\t)*(.+)/m.test(state.unit)) {
state.unit.replace(/^(#{1,6})(?: |\t)*(.+)(?: |\t)*(?:#{0,6})(?: |\t)*$/m,
function(all, g1, g2, offset, _str) {
var pre = state.unit.substr(0, offset);
var post = state.unit.substr(offset + all.length);
var header = '<h' + g1.length + '>' + g2 + '</h' + g1.length + '>';
var innerFormatter = buildFormatterClone(state);
if (pre) {
state.unitDone.push(innerFormatter.format(pre));
}
innerFormatter.wrapRemainingInParaOrig = false;
state.unitDone.push(innerFormatter.format(header));
if (post) {
state.remaining = post + '\n\n' + state.remaining;
}
state.unit = '';
}
);
} else {
state.invokeNextBlockFormatter();
}
};
}
function getBlockHeadersUnderline() {
return function(state) {
if (/^(?: |\t)*(.+?)(?: |\t)*\n(=+|-+)(?: |\t)*$/m.test(state.unit)) {
state.unit.replace(/^(?: |\t)*(.+?)(?: |\t)*\n(=+|-+)(?: |\t)*$/m,
function(all, g1, g2, offset, _str) {
var pre = state.unit.substr(0, offset);
var post = state.unit.substr(offset + all.length);
var number = g1.match(/^=/) ? 1 : 2;
var header = '<h' + number + '>' + g1 + '</h' + number + '>';
var innerFormatter = buildFormatterClone(state);
if (pre) {
state.unitDone.push(innerFormatter.format(pre));
}
innerFormatter.wrapRemainingInParaOrig = false;
state.unitDone.push(innerFormatter.format(header));
if (post) {
state.remaining = post + '\n\n' + state.remaining;
}
state.unit = '';
}
);
} else {
state.invokeNextBlockFormatter();
}
}
}
function getBlockHR() {
return function(state) {
if (/^((( |\t)*\*){3,}( |\t)*)|((( |\t)*-){3,}( |\t)*)$/m.test(state.unit)) {
state.unit.replace(/^(?:(?:(?: |\t)*\*){3,}(?: |\t)*)|(?:(?:(?: |\t)*-){3,}(?: |\t)*)$/m,
function(all, offset, _str) {
var pre = state.unit.substr(0, offset);
var post = state.unit.substr(offset + all.length);
var innerFormatter = buildFormatterClone(state);
if (pre) {
state.unitDone.push(innerFormatter.format(pre));
}
state.unitDone.push('<hr />');
if (post) {
state.remaining = post + '\n\n' + state.remaining;
}
state.unit = '';
}
);
} else {
state.invokeNextBlockFormatter();
}
};
}
function getSpanWikiLinker() {
return function (state) {
state.unit = state.unit.replace(/\b[A-Z][^ ]*[a-z][A-Z][^ ]*\b/g,
function(matched, _str, _offset) {
return makeWikiLinkString(matched, state.knownPages);
}
);
state.invokeNextSpanFormatter();
};
}
function getSpanLinker() {
return function(state) {
state.unit = state.unit.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
function(_matched, g1, g2, _str, _offset) {
var title = g2.match(/ "([^"]+)"\s*$/);
if (null == title || 0 == title.length) {
return '<a href="' + g2 + '">' + g1 + '</a>';
} else {
var href = g2.match(/^(.+) "([^"]+)"\s*$/)[1];
title = title[1];
return '<a href="' + href + '" title="' + title + '">' + g1 + '</a>';
}
}
);
state.invokeNextSpanFormatter();
};
}
function getSpanImager() {
return function(state) {
state.unit = state.unit.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,
function(_matched, g1, g2, _str, _offset) {
var title = g2.match(/ "([^"]+)"\s*$/);
if (null == title || 0 == title.length) {
return '<img src="' + g2 + '" alt="' + g1 + '" />';
} else {
var href = g2.match(/^(.+) "([^"]+)"\s*$/)[1];
title = title[1];
return '<img src="' + href + '" title="' + title + '" alt="' + g1 + '" />';
}
}
);
state.invokeNextSpanFormatter();
};
}
function getSpanBR() {
return function (state) {
state.unit = state.unit.replace(/\s{2,}$/mg, '<br />');
state.invokeNextSpanFormatter();
};
}
function getSpanBoldItalic() {
return function (state) {
var text = state.unit;
text = text.replace(/(__)(.+?)(__)/g, "<strong>$2</strong>");
text = text.replace(/(\*\*)(.+?)(\*\*)/g, "<strong>$2</strong>");
text = text.replace(/(_)(.+?)(_)/g, "<em>$2</em>");
text = text.replace(/(\*)(.+?)(\*)/g, "<em>$2</em>");
state.unit = text;
state.invokeNextSpanFormatter();
};
}
function getSpanCode() {
return function (state) {
state.unit = state.unit.replace(/(`+)((?:.|[\n\r])+?)\1/g,
function(_matched, _g1, g2, _str, _offset) {
return "<code>" + encodeAmpsAngles(g2) + "</code>";
}
);
state.invokeNextSpanFormatter();
};
}
function getSpanAutoLinks() {
return function (state) {
state.unit = state.unit.replace(/<([^ @\f\n\r\t]+?@[^ @\f\n\r\t]+?)>/g,
'<a href="mailto:$1">$1</a>');
state.unit = state.unit.replace(/<((?:http(?:s)?|ftp|gopher):\/\/[^ \f\n\r\t]+)>/g,
'<a href="$1">$1</a>');
state.invokeNextSpanFormatter();
};
}
function getSpanReferenceSlurper() {
return function(state) {
state.unit = state.unit.replace(/^[ \t]*(!?)\[([^\]]+)\]:[ \t]+<?([^ \f\n\r\t]+)>?(?:[ \t]+(?:\n[ \t]*)?(?:(?:(["'])(.+)\4)|(?:\((.+)\))))?[ \t]*$\n?/mg,
function(_matched, bang, id, url, _sep, title1, title2, _str, _offset) {
var title = title1 ? title1 : title2;
var linkObj = new Object();
linkObj.title = title;
linkObj.url = url;
linkObj.id = id;
if (bang) {
state.imgRefs.push(linkObj);
} else {
state.linkRefs.push(linkObj);
}
return '';
}
);
state.invokeNextSpanFormatter();
};
}