Javascript to convert Markdown/Textile to HTML (and, ideally, back to Markdown/Textile)

Viewed 89579

There are several good Javascript editors for Markdown / Textile (e.g.: http://attacklab.net/showdown/, the one I'm using right now), but all I need is a Javascript function that converts a string from Markdown / Textile -> HTML and back.

What's the best way to do this? (Ideally it would be jQuery-friendly -- e.g., $("#editor").markdown_to_html())

Edit: Another way to put it is that I'm looking for a Javascript implementation of Rails' textilize() and markdown() text helpers

13 Answers

I thought it would be worthwhile to make a list here of the JavaScript solutions out there and their minified (uncompressed) size and strengths/weaknesses. Compressed size for minified code will be around 50% of uncompressed size. What it comes down to:

  • Use markdown-it (104KB) if you need comprehensive support and will have user edited or arbitrary documents, but are not overly concerned with size/bandwidth.
  • Use my own drawdown (1.3KB) if you need reasonably high quality, and table support, but want feather weight and do not need any features other than conversion, or to have every single edge case addressed.
  • Use one of the others if you need unique capabilities like security or expandability.

All of these use the MIT license, most are on npm.

  • markdown-it: 104KB. Powers StackExchange since the CommonMark migration. Follows the CommonMark spec and is now more or less the gold standard; supports syntax extensions; produces secure output by default. Fast; as robust as showdown, but very large. Has a ton of features (e.g. synced scrolling). Is also the basis for http://dillinger.io/.

  • showdown: 28KB. Has comprehensive CommonMark support and was previously the gold standard; is significantly smaller than Markdown-It but slower. it is the basis for pagedown.

  • pagedown: 8KB. Powered StackExchange before the CommonMark migration. It is very robust but missing tables, definition lists, footnotes, etc. In addition to the 8KB converter script, it also offers editor and sanitizer scripts.

  • drawdown: 1.3KB. Full disclosure, I wrote it. Broader feature scope than any other lightweight converter; handles most but not all of the CommonMark spec. Not recommended for user editing but very useful for presenting information in web apps. No inline HTML.

  • marked: 19KB. Comprehensive; tested against unit test suite; supports custom lexer rules.

  • micromarkdown: 5KB. Supports a lot of features, but is missing some common ones like unordered lists using * and some common ones that aren't strictly part of the spec like fenced code blocks. Many bugs, throws exceptions on most longer documents. I consider it experimental.

  • nano-markdown: 1.9KB. Feature scope limited to things used by most documents; more robust than micromarkdown but not perfect; uses its own very basic unit test. Reasonably robust but breaks on many edge cases.

  • mmd.js: 800 bytes. The result of an effort to make the smallest possible parser that is still functional. Supports a small subset; document needs to be tailored for it.

  • markdown-js: 54KB (not available for download minified; would probably minify to ~20KB). Looks pretty comprehensive and includes tests, but I'm not very familiar with it.

  • meltdown: 41KB (not available for download minified; would probably minify to ~15KB). jQuery plugin; Markdown Extra (tables, definition lists, footnotes).

  • unified.js: varies, 5-100KB. A plugin-based system for converting between html, markdown, and prose. Depending on what plugins you need (spell-checking, syntax-highlighting, input sanitizing) the file size will vary. Probably used more server-side than client-side.

Textile

You can find a seemingly very fine Javascript implementation of Textile here, and another one there (maybe not so good, but has a nice convert-as-you-type example page).

Note: there is a bug in the first implementation I made a link to : horizontal bars are not rendered correctly. To fix it, you can add the following code in the file.

for(i=0;i<lines.length;i++) {
    // Add code :Start
    if (lines[i].match(/\s*-{4,}\s*/)){
      html+="<hr/>\n";
      continue;
    }
    // Add code :End
    if (lines[i].indexOf("[") == 0) {continue;}
    //...

Let's write a simple markdown parser function that will take in a single line of markdown and be translated into the appropriate HTML. To keep it simple, we'll support only one feature of markdown in syntax: headers.

Headers are designated by (1-6) hashes followed by a space, followed by text. The number of hashes determines the header level of the HTML output.

function markdownParser(markdown) {
  const htmlText = markdown
    .replace(/^# (.*$)/gim, '<h1>$1</h1>')
    .replace(/^## (.*$)/gim, '<h2>$1</h2>')
    .replace(/^### (.*$)/gim, '<h3>$1</h3>')
    .replace(/^#### (.*$)/gim, '<h4>$1</h4>')
    .replace(/^##### (.*$)/gim, '<h5>$1</h5>')
    .replace(/^###### (.*$)/gim, '<h6>$1</h6>')
 return htmlText.trim() 
}

Have you looked at the Eclipse WikiText library that is part of Mylyn. It will convert from many wiki syntax to xhtml and to xdocs/DITA. It looks way cool.

http://help.eclipse.org/galileo/topic/org.eclipse.mylyn.wikitext.help.ui/help/Markup-Conversion.html

Has anyone found a solution to the HTML->textile problem? All of our current documentation is in M$ Word format and we would love to bring it into Redmine Wiki for collaborative maintenance. We have not found any tool that will make the conversion. We have found the Open Office extension that produces mediawiki formatted text but Redmine Wiki uses a subset of textile.

Anyone know of a tool that converts TO textile from mediawiki, Word, XDocs, or HTML?

I found the answer by karim79 useful. I modified it a bit, so I want to share my version. Maybe it will help someone out there.

I modified the answer to adapt it from JavaScript to PHP (I know the question doesn't have a PHP tag)

I also changed from while() to for() because:

  1. I didn't want to replace uninterrupted sequences of asterisks like "********"
  2. I didn't want to remove unpaired "*"

It's still not perfect, it will for example "strongify" anything between two far apart *'s, consuming both of them. But I think this is how whatsapp behaves also.

I assume there are better ways of forcing the end of the loop than what I did, I just followed the rule "if it aint broken, don't fix it". I'm happy to receive suggestions

    function md2html($text = "") {
        // I constantly check for length of text, instead of assigning the value to a var (which would be more efficient) because the length changes with every replacement! 
        for ($i = 0; $i < strlen($text); $i++) {
            if (($beg = strpos($text,"*",$i)) !== FALSE){
                if (($seq = strspn($text,"*",$beg)) > 1) 
                    $i = $beg + $seq; // skip sequence
                else {
                    if (($end = strpos($text,"*",$beg + 1)) !== FALSE){ // found a second one
                        $length = $end - $beg - 1;
                        $innerText = substr($text,$beg+1,$length);
                        $strongified = "<strong>$innerText</strong>";
                        // following the original answer I should do something like
                        //   $text = substr($text,0,$beg) . $strongified . substr($text,$end+1);
                        // but I assume the following is better:
                        $text = substr_replace($text,$strongified,$beg,$length+2); // adding to length to include both asterisks 
                        $i = $end + 16; // the real position in the string has now increased due to replacement
                    } else
                        $i = strlen($text); // force end of for loop
                }
            } else
                $i = strlen($text);
        }
        for ($i = 0; $i < strlen($text); $i++) {
            if (($beg = strpos($text,"_",$i)) !== FALSE){
                if (($seq = strspn($text,"_",$beg)) > 1)
                    $i = $beg + $seq;
                else {
                    if (($end = strpos($text,"_",$beg + 1)) !== FALSE){
                        $length = $end - $beg - 1;
                        $innerText = substr($text,$beg+1,$length);
                        $italicized = "<em>$innerText</em>";
                        $text = substr_replace($text,$italicized,$beg,$length+2);
                        $i = $end + 10;
                    } else
                        $i = strlen($text);
                }
            } else
                $i = strlen($text);
        }
        return $text;
    }
Related