Skip to content

Instantly share code, notes, and snippets.

@renehamburger
Last active September 4, 2023 07:55
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save renehamburger/12f14a9bd9297394e5bd to your computer and use it in GitHub Desktop.
Save renehamburger/12f14a9bd9297394e5bd to your computer and use it in GitHub Desktop.
slimdown.js
'use strict';
/**
* Javascript version of https://gist.github.com/jbroadway/2836900
*
* Slimdown - A very basic regex-based Markdown parser. Supports the
* following elements (and can be extended via Slimdown::add_rule()):
*
* - Headers
* - Links
* - Bold
* - Emphasis
* - Deletions
* - Quotes
* - Inline code
* - Blockquotes
* - Ordered/unordered lists
* - Horizontal rules
*
* Author: Johnny Broadway <johnny@johnnybroadway.com>
* Website: https://gist.github.com/jbroadway/2836900
* License: MIT
*/
function Slimdown() {
// Rules
this.rules = [
{regex: /(#+)(.*)/g, replacement: header}, // headers
{regex: /!\[([^\[]+)\]\(([^\)]+)\)/g, replacement: '<img src=\'$2\' alt=\'$1\'>'}, // image
{regex: /\[([^\[]+)\]\(([^\)]+)\)/g, replacement: '<a href=\'$2\'>$1</a>'}, // hyperlink
{regex: /(\*\*|__)(.*?)\1/g, replacement: '<strong>$2</strong>'}, // bold
{regex: /(\*|_)(.*?)\1/g, replacement: '<em>$2</em>'}, // emphasis
{regex: /\~\~(.*?)\~\~/g, replacement: '<del>$1</del>'}, // del
{regex: /\:\"(.*?)\"\:/g, replacement: '<q>$1</q>'}, // quote
{regex: /`(.*?)`/g, replacement: '<code>$1</code>'}, // inline code
{regex: /\n\*(.*)/g, replacement: ulList}, // ul lists
{regex: /\n[0-9]+\.(.*)/g, replacement: olList}, // ol lists
{regex: /\n(&gt;|\>)(.*)/g, replacement: blockquote}, // blockquotes
{regex: /\n-{5,}/g, replacement: '\n<hr />'}, // horizontal rule
{regex: /\n([^\n]+)\n/g, replacement: para}, // add paragraphs
{regex: /<\/ul>\s?<ul>/g, replacement: ''}, // fix extra ul
{regex: /<\/ol>\s?<ol>/g, replacement: ''}, // fix extra ol
{regex: /<\/blockquote><blockquote>/g, replacement: '\n'} // fix extra blockquote
];
// Add a rule.
this.addRule = function (regex, replacement) {
regex.global = true;
regex.multiline = false;
this.rules.push({regex: regex, replacement: replacement});
};
// Render some Markdown into HTML.
this.render = function (text) {
text = '\n' + text + '\n';
this.rules.forEach(function (rule) {
text = text.replace(rule.regex, rule.replacement);
});
return text.trim();
};
function para (text, line) {
debugger;
var trimmed = line.trim();
if (/^<\/?(ul|ol|li|h|p|bl)/i.test(trimmed)) {
return '\n' + line + '\n';
}
return '\n<p>' + trimmed + '</p>\n';
}
function ulList (text, item) {
return '\n<ul>\n\t<li>' + item.trim() + '</li>\n</ul>';
}
function olList (text, item) {
return '\n<ol>\n\t<li>' + item.trim() + '</li>\n</ol>';
}
function blockquote (text, tmp, item) {
return '\n<blockquote>' + item.trim() + '</blockquote>';
}
function header (text, chars, content) {
var level = chars.length;
return '<h' + level + '>' + content.trim() + '</h' + level + '>';
}
}
var sd = new Slimdown();
$('body').append(sd.render('# Title\n\nAnd *now* [a link](http://www.google.com) to **follow** and [another](http://yahoo.com/).\n\n* One\n* Two\n* Three\n\n## Subhead\n\nOne **two** three **four** five.\n\nOne __two__ three _four_ five __six__ seven _eight_.\n\n1. One\n2. Two\n3. Three\n\nMore text with `inline($code)` sample.\n\n> A block quote\n> across two lines.\nMore text...'));
@fabienvauchelles
Copy link

This is the ES6 version:

'use strict';

/**
 * Javascript version of https://gist.github.com/jbroadway/2836900
 *
 * Slimdown - A very basic regex-based Markdown parser. Supports the
 * following elements (and can be extended via Slimdown::add_rule()):
 *
 * - Headers
 * - Links
 * - Bold
 * - Emphasis
 * - Deletions
 * - Quotes
 * - Inline code
 * - Blockquotes
 * - Ordered/unordered lists
 * - Horizontal rules
 *
 * Author: Johnny Broadway <johnny@johnnybroadway.com>
 * Website: https://gist.github.com/jbroadway/2836900
 * License: MIT
 */
class Slimdown {

    constructor() {
        // Rules
        this.rules = [
            {regex: /(#+)(.*)/g, replacement: header},                                         // headers
            {regex: /!\[([^\[]+)\]\(([^\)]+)\)/g, replacement: '<img src=\'$2\' alt=\'$1\'>'}, // image
            {regex: /\[([^\[]+)\]\(([^\)]+)\)/g, replacement: '<a href=\'$2\'>$1</a>'},        // hyperlink
            {regex: /(\*\*|__)(.*?)\1/g, replacement: '<strong>$2</strong>'},                  // bold
            {regex: /(\*|_)(.*?)\1/g, replacement: '<em>$2</em>'},                             // emphasis
            {regex: /\~\~(.*?)\~\~/g, replacement: '<del>$1</del>'},                           // del
            {regex: /\:\"(.*?)\"\:/g, replacement: '<q>$1</q>'},                               // quote
            {regex: /`(.*?)`/g, replacement: '<code>$1</code>'},                               // inline code
            {regex: /\n\*(.*)/g, replacement: ulList},                                         // ul lists
            {regex: /\n[0-9]+\.(.*)/g, replacement: olList},                                   // ol lists
            {regex: /\n(&gt;|\>)(.*)/g, replacement: blockquote},                              // blockquotes
            {regex: /\n-{5,}/g, replacement: '\n<hr />'},                                      // horizontal rule
            {regex: /\n([^\n]+)\n/g, replacement: para},                                       // add paragraphs
            {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
            {regex: /<\/ol>\s?<ol>/g, replacement: ''},                                        // fix extra ol
            {regex: /<\/blockquote><blockquote>/g, replacement: '\n'}                          // fix extra blockquote
        ];


        ////////////

        function para(text, line) {
            const trimmed = line.trim();
            if (/^<\/?(ul|ol|li|h|p|bl)/i.test(trimmed)) {
                return `\n${line}\n`;
            }
            return `\n<p>${trimmed}</p>\n`;
        }

        function ulList(text, item) {
            return `\n<ul>\n\t<li>${item.trim()}</li>\n</ul>`;
        }

        function olList(text, item) {
            return `\n<ol>\n\t<li>${item.trim()}</li>\n</ol>`;
        }

        function blockquote(text, tmp, item) {
            return `\n<blockquote>${item.trim()}</blockquote>`;
        }

        function header(text, chars, content) {
            const level = chars.length;
            return `<h${level}>${content.trim()}</h${level}>`;
        }
    }

    // Add a rule.
    addRule(regex, replacement) {
        regex.global = true;
        regex.multiline = false;
        this.rules.push({
            regex,
            replacement,
        });
    }

    // Render some Markdown into HTML.
    render(text) {
        text = `\n${text}\n`;
        this.rules.forEach( (rule) => {
            text = text.replace(rule.regex, rule.replacement);
        });

        return text.trim();
    }
}


////////////

module.exports = Slimdown;

@erikvullings
Copy link

I discovered your version too later, after starting to work on the slimdown PHP version as you have. You can find my (slightly improved) version on GitHub and on NPM, and on the playground.

My improvements are mainly related to adding images, tables and codeblocks, and some other fixes, and I did add a link to your version too.

@mmuman
Copy link

mmuman commented Sep 3, 2023

I tried using it in a little web app to import markdown from Nextcloud and Etherpad, however I found some issues:

  • it finds headers when there are none, including inside URL fragments,
  • underscores inside URLs are turned into EM or STRONG,
  • Etherpad seems to start UL with " *" so they aren't detected,
  • dashes are not handled as UL,
  • all newlines are converted into paragraphs, when actually only two in a row should mark end of paragraph, single newlines should covnert to <br/>,
  • Escaped chars with \ are not de-escaped.

Let's see if I can fix that… Yeah, this seems to work for me (I didn't touch OL as I don't use them):

// from https://gist.github.com/renehamburger/12f14a9bd9297394e5bd

'use strict';

/**
 * Javascript version of https://gist.github.com/jbroadway/2836900
 *
 * Slimdown - A very basic regex-based Markdown parser. Supports the
 * following elements (and can be extended via Slimdown::add_rule()):
 *
 * - Headers
 * - Links
 * - Bold
 * - Emphasis
 * - Deletions
 * - Quotes
 * - Inline code
 * - Blockquotes
 * - Ordered/unordered lists
 * - Horizontal rules
 *
 * Author: Johnny Broadway <johnny@johnnybroadway.com>
 * Website: https://gist.github.com/jbroadway/2836900
 * License: MIT
 */
function Slimdown() {

  // Rules
  this.rules =  [
    {regex: /\n(#+)(.*)/g, replacement: header},                                         // headers
    {regex: /!\[([^\[]+)\]\(([^\)]+)\)/g, replacement: '<img src=\'$2\' alt=\'$1\'>'}, // image
    {regex: /\[([^\[]+)\]\(([^\)]+)\)/g, replacement: '<a href=\'$2\'>$1</a>'},        // hyperlink
// _ can be used in URLs and we do not want to mess them up.
//    {regex: /(\*\*|__)(.*?)\1/g, replacement: '<strong>$2</strong>'},                // bold
//    {regex: /(\*\*)(.*?)\1/g, replacement: '<strong>$2</strong>'},                   // bold
    {regex: /([^\\])(\*\*|__)(.*?)\2/g, replacement: '$1<strong>$3</strong>'},         // bold
//    {regex: /(\*|_)(.*?)\1/g, replacement: '<em>$2</em>'},                           // emphasis
//    {regex: /(\*)(.*?)\1/g, replacement: '<em>$2</em>'},                             // emphasis
    {regex: /([^\\])(\*|_)(.*?)\2/g, replacement: '$1<em>$3</em>'},                    // emphasis
    {regex: /\~\~(.*?)\~\~/g, replacement: '<del>$1</del>'},                           // del
    {regex: /\:\"(.*?)\"\:/g, replacement: '<q>$1</q>'},                               // quote
    {regex: /`(.*?)`/g, replacement: '<code>$1</code>'},                               // inline code
    {regex: /\n( *)(\*|-)(.*)/g, replacement: ulList},                                         // ul lists
    {regex: /\n[0-9]+\.(.*)/g, replacement: olList},                                   // ol lists
    {regex: /\n(&gt;|\>)(.*)/g, replacement: blockquote},                              // blockquotes
    {regex: /\n-{5,}/g, replacement: '\n<hr />'},                                      // horizontal rule
    {regex: /\n\n([^\n]+)\n\n/g, replacement: para},                                       // add paragraphs
    {regex: /([^>])\\?\n([^<])/g, replacement: '$1<br/>$2'},                           // newlines
    {regex: /\\(.)/g, replacement: '$1'},                                              // unescape
    {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
    {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
    {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
    {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
    {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
    {regex: /<\/ul>\s?<ul>/g, replacement: ''},                                        // fix extra ul
    {regex: /<\/ol>\s?<ol>/g, replacement: ''},                                        // fix extra ol
    {regex: /<\/blockquote><blockquote>/g, replacement: '\n'}                          // fix extra blockquote
  ];

  // Add a rule.
  this.addRule = function (regex, replacement) {
    regex.global = true;
    regex.multiline = false;
    this.rules.push({regex: regex, replacement: replacement});
  };

  // Render some Markdown into HTML.
  this.render = function (text) {
    text = '\n' + text + '\n';
    this.rules.forEach(function (rule) {
      text = text.replace(rule.regex, rule.replacement);
    });
    return text.trim();
  };

  function para (text, line) {
    //debugger;
    var trimmed = line.trim();
    if (/^<\/?(ul|ol|li|h|p|bl)/i.test(trimmed)) {
      return '\n' + line + '\n';
    }
    return '\n<p>' + trimmed + '</p>\n';
  }

  function ulList (text, level, bullet, item) {
    level = 1+Math.floor(level.length/2);
    // Not really correct but seems to work in Firefox and Chromium at least
    return '\n<ul>'.repeat(level) + '\n\t<li>' + item.trim() + '</li>' + '\n</ul>'.repeat(level);
  }

  function olList (text, item) {
    return '\n<ol>\n\t<li>' + item.trim() + '</li>\n</ol>';
  }

  function blockquote (text, tmp, item) {
    return '\n<blockquote>' + item.trim() + '</blockquote>';
  }

  function header (text, chars, content) {
    var level = chars.length;
    return '\n<h' + level + '>' + content.trim() + '</h' + level + '>';
  }
}

@erikvullings
Copy link

@mmuman Pretty cool - thanks for sharing your solution!
There is also a npm package with the same name, https://github.com/erikvullings/slimdown-js, so if you would like to submit a PR, I will gladly include it. If this is too much work, let me know, and I'll do it instead (but I prefer to give you the credits :-))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment