String.prototype.replaceAll = function(find, replace) {
if (typeof find == 'string') return this.split(find).join(replace);
var t = this, i, j;
while (typeof(i = find.shift()) == 'string' && typeof(j = replace.shift()) == 'string') t = t.replaceAll(i || '', j || '');
return t;
};
function html(input, replaceQuoteOff) {
if (replaceQuoteOff) return input.toString().replaceAll(['&', '<'], ['&', '<']);
return input.toString().replaceAll(['&', '<', '"'], ['&', '<', '"']);
}
function warning(message) {
console.log(message);
}
function spanMarkdown(input) {
input = html(input);
while (input.match(/\^([\w\^]+)/)) input = input.replace(/\^([\w\^]+)/, '<sup>$1</sup>');
return input
.replaceAll('\u0001', '^')
.replace(/\[(.+?)\|(.+?)\]/g, '<abbr title="$2">$1</abbr>')
.replaceAll('\u0002', '[')
.replace(/\[\[(\d+)\](.*?)\]/g, '<sup class="reference" title="$2">[$1]</sup>')
.replace(/!\[([^\]]+)]\((https?:\/\/[^\s("\\]+\.[^\s"\\]+)\)/g, '<img alt="$1" src="$2" />')
.replace(/^(https?:\/\/([^\s("\\]+\.[^\s"\\]+\.(svg|png|tiff|jpg|jpeg)(\?[^\s"\\\/]*)?))/g, '<img src="$1" />')
.replace(/\[([^\]]+)]\((https?:\/\/[^\s("\\]+\.[^\s"\\]+)\)/g, '$1'.link('$2'))
.replace(/([^;["\\])(https?:\/\/([^\s("\\]+\.[^\s"\\]+\.(svg|png|tiff|jpg|jpeg)(\?[^\s"\\\/]*)?))/g, '$1<img src="$2" />')
.replace(/([^;["\\])(https?:\/\/([^\s("\\]+\.[^\s"\\]+))/g, '$1' + '$3'.link('$2'))
.replace(/^(https?:\/\/([^\s("\\]+\.[^\s"\\]+))/g, '$2'.link('$1'));
}
function inlineMarkdown(input) {
var output = '',
span = '',
current = [],
tags = {
'`': 'code',
'``': 'samp',
'*': 'em',
'**': 'strong',
'_': 'i',
'–––': 's',
'+++': 'ins',
'---': 'del',
'[c]': 'cite',
'[m]': 'mark',
'[u]': 'u',
'[v]': 'var',
'::': 'kbd',
'"': 'q'
},
stags = {
sup: {
start: '^(',
end: ')^'
},
sub: {
start: 'v(',
end: ')v'
},
small: {
start: '[sm]',
end: '[/sm]'
}
};
outer: for (var i = 0; i < input.length; i++) {
if (['code', 'samp'].indexOf(current[current.length - 1]) == -1) {
if (input[i] == '\\') span += input[++i].replace('^', '\u0001').replace('[', '\u0002');
else {
for (var l = 3; l > 0; l--) {
if (tags[input.substr(i, l)]) {
output += spanMarkdown(span);
span = '';
if (current[current.length - 1] == tags[input.substr(i, l)]) output += '</' + current.pop() + '>';
else {
if (current.indexOf(tags[input.substr(i, l)]) != -1) warning('Illegal nesting of "' + input.substr(i, l) + '"');
output += '<' + tags[input.substr(i, l)] + '>';
current.push(tags[input.substr(i, l)]);
}
i += l - 1;
continue outer;
}
}
for (var j in stags) {
for (var l = 5; l > 0; l--) {
if (stags[j].start == input.substr(i, l)) {
output += spanMarkdown(span) + '<' + j + '>';
span = '';
current.push(stags[j].end);
i += l - 1;
continue outer;
} else if (stags[j].end == input.substr(i, l)) {
if (current[current.length - 1] == stags[j].end) {
output += spanMarkdown(span) + '</' + j + '>';
span = '';
current.pop();
i += l - 1;
continue outer;
} else warning('Illegal close tag "' + stags[j].end + '" found');
}
}
}
span += input[i];
}
} else if (current[current.length - 1] == 'code' && input[i] == '`') {
current.pop();
output += '</code>';
} else if (current[current.length - 1] == 'samp' && input.substr(i, 2) == '``') {
current.pop();
output += '</samp>';
i++;
} else output += html(input[i]);
}
output += spanMarkdown(span);
if (current.length) warning('Unclosed tags. <' + current.join('>, <') + '>');
for (var i = current.length - 1; i >= 0; i--) output += '</' + current[i] + '>';
return output;
}
This only parses inline markdown and converts it to HTML (on both node.js and client-side). It doesn't conform to commonmark or any other specification. This is related to:
- Markdown to HTML which is a blob of regexps
- Markdown to HTML, again which has an… interesting (confusing) split/map nesting (and didn't work with XHTML)
This one basically goes thru character by character doing things based on the current state of the machine, similar the the (block) markdown function (that goes line by line) in the second question above ^.
replaceAll()
is used everywhere on my app, so it's not going to change, and I don't think fiddling with String.prototype is wrong.html()
does an HTML escape. It doesn't escape everything and doesn't work for all cases, but I'm happy enough.warning()
is just a function that collections whatever complaints inlineMarkdown has. This is just a console.log for testing, but I display the warnings to the user when using it client-side.spanMarkdown()
deals with linkifying and simple inline-markdown things that can be done with regex – it's easy to add stuff like oneboxing here.inlineMarkdown()
parses teh markdownz! (and depends on the other functions)
tags
contains simple tags, which have equivalent start and end markdown sequences and cannot be nested within themselves, while stags
contains special tags which have different start and end tags. When looking for tags, it goes thru a loop testing to see if substrings of each length match, which looks messy.
I don't know whether I should make non-parsed tags (code and samp) a dedicated expandable block so I can add any more without special-casing them.
This parser is also pretty picky, so I've also got a (client-side) function to complain when a user enters markdown that doesn't make sense:
HTMLTextAreaElement.prototype.mdValidate = function(correct) {
var i = mdWarnings.length;
markdown(this.value);
var preverr = this.previousSibling && this.previousSibling.classList.contains('md-err') ? this.previousSibling : null,
err = mdWarnings[i];
this.lastErrored = err && correct;
if (err && (correct || preverr || this.value.substr(0, this.selectionEnd || Infinity).match(/\s$/))) {
if (preverr) {
if (preverr.firstChild.nodeValue == err) {
if (this.lastErrored && err && correct) {
var input = this.value,
output = '',
span = '',
current = [],
tags = {
'`': 'code',
'``': 'samp',
'*': 'em',
'**': 'strong',
'_': 'i',
'–––': 's',
'+++': 'ins',
'---': 'del',
'[c]': 'cite',
'[m]': 'mark',
'[u]': 'u',
'[v]': 'var',
'::': 'kbd',
'"': 'q'
},
stags = {
sup: {
start: '^(',
end: ')^'
},
sub: {
start: 'v(',
end: ')v'
},
small: {
start: '[sm]',
end: '[/sm]'
}
};
outer: for (var i = 0; i < input.length; i++) {
if (['code', 'samp'].indexOf(current[current.length - 1]) == -1) {
if (input[i] == '\\') span += input[++i];
else {
for (var l = 4; l >= 0; l--) {
if (tags[input.substr(i, l)]) {
output += span;
span = '';
if (['code', 'samp'].indexOf(tags[input.substr(i, l)]) == -1) output += '\\' + input.substr(i, l);
else if (current[current.length - 1] == tags[input.substr(i, l)]) {
current.pop();
output += '\\' + input.substr(i, l);
} else {
output += '\\' + input.substr(i, l);
current.push(tags[input.substr(i, l)]);
}
i += l - 1;
continue outer;
}
}
for (var j in stags) {
for (var l = 5; l >= 0; l--) {
if (stags[j].start == input.substr(i, l)) {
output += span + '\\' + input.substr(i, l);
span = '';
i += l - 1;
continue outer;
} else if (stags[j].end == input.substr(i, l)) {
if (current[current.length - 1] == stags[j].end) {
output += span + '\\' + input.substr(i, l);
span = '';
i += l - 1;
continue outer;
}
}
}
}
span += input[i];
}
} else if (current[current.length - 1] == 'code' && input[i] == '`') {
current.pop();
output += '`';
} else if (current[current.length - 1] == 'samp' && input.substr(i, 2) == '``') {
current.pop();
output += '``';
i++;
} else output += input[i];
}
output += span;
if (current[current.length - 1] == 'code' && input[i] == '`') {
output += '`';
} else if (current[current.length - 1] == 'samp' && input.substr(i, 2) == '``') {
output += '``';
}
this.value = output;
return true;
}
return err;
}
preverr.parentNode.removeChild(preverr);
}
var span = document.createElement('span');
span.classList.add('md-err');
span.appendChild(document.createTextNode(err));
this.parentNode.insertBefore(span, this);
} else if (preverr) preverr.parentNode.removeChild(preverr);
return err;
};
function mdValidateBody() {
setTimeout(function(e) {
e.mdValidate();
}, 0, document.activeElement);
}
Basically, when a form is submitted, <textarea>
s with markdown are scanned for errors with
if (mytextarea.mdValidate(true)) ohnoes.stopFormSubmission()
If they exist the function will display an error message. If they submit again, it will automatically escape the markdown in an attempt to correct (the boolean argument) the error, using some of the same code from inlineMarkdown.
What can I do to improve these functions?