7
\$\begingroup\$
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(['&', '<'], ['&amp;', '&lt;']);
    return input.toString().replaceAll(['&', '<', '"'], ['&amp;', '&lt;', '&quot;']);
}
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:

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?

\$\endgroup\$
2
\$\begingroup\$

My first comment would be to add some blank lines between those functions of yours. You should also add some comments describing the purpose of the functions as well. My preferred style of documentation is usually JSDoc, but you can always find a different style as well.

Secondly, even though you say that you're fine with patching Javascript's builtin object prototypes, it's still a really bad idea. See this Stackoverflow question for more details on that.

From looking at your code, I've also noticed that you don't use braces in many places, like here:

if (typeof find == 'string') return this.split(find).join(replace);

Generally it's good to use braces, even if you're only running one line of code. A famous example of this ending up very badly is the Apple SSL bug. The bug looked something like this:

if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
    goto fail;
    goto fail;  /* MISTAKE! THIS LINE SHOULD NOT BE HERE */

So, in short, use braces.

Other than that, your code looks really nice!

\$\endgroup\$
  • 1
    \$\begingroup\$ Omitting braces is not that bad if the action is on the same line as the condition. \$\endgroup\$ – 200_success Jul 12 '15 at 17:13
  • \$\begingroup\$ @200_success While it's not bad, I generally find it to be better to include braces anyways. \$\endgroup\$ – Ethan Bierlein Jul 12 '15 at 17:15
  • \$\begingroup\$ JSDoc seems to focus on what things do, rather than how they do them, or at least that's what the examples seem to show. I don't use libraries or support old browsers so using built-in object prototypes is ok for me. \$\endgroup\$ – bjb568 Jul 12 '15 at 17:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.