Skip to content

Extensions

Estevão Soares dos Santos edited this page Sep 14, 2018 · 12 revisions

Showdown allows additional functionality to be loaded via extensions.

Client-side Extension Usage

<script src="src/showdown.js"></script>
<script src="src/extensions/twitter.js"></script>
<script>
var converter = new showdown.Converter({ extensions: ['twitter'] });
</script>

Server-side Extension Usage

// Using a bundled extension
var showdown = require('showdown');
var converter = new showdown.Converter({ extensions: ['twitter'] });

// Using a custom extension
var mine = require('./custom-extensions/mine');
var converter = new showdown.Converter({ extensions: ['twitter', mine] });

CLI Extension Usage

Extensions can be loaded while using the CLI tool using the -e flag.

showdown -e twitter -i foo.md -o bar.html

Creating Showdown Extensions

A showdown extension is simply a function which returns an array of language or output or listener (coming soon) extensions (henceforth called "sub-extensions").

var myext = function () {
  var myext1 = {
    type: 'lang',
    regex: /markdown/g,
    replace: 'showdown'
  };
  var myext2 = {
    /* extension code */
  };
  return [myext1, myext2];
}

Each sub-extension (corresponds to myext1 and myext2 in the above example) should be an object that defines the behavior of said sub-extension.

Sub-extension object properties

Sub extensions object should have a type property (that defines the type of the sub-extension) and either a regex and a replace property or a filter property.

Type property(required)

Type property defines the nature of said sub-extensions and can assume 2 values:

  • lang - Language extensions add new markdown syntax to showdown.

    For example, say you wanted ^^youtube http://www.youtube.com/watch?v=oHg5SJYRHA0 to automatically render as an embedded YouTube video, that would be a language extension.

    They have the highest priority in the subparser order, which means they are called right after the input text is escaped and normalized and before any other subparser (or extension) is called.

  • output - Output extensions (or modifiers) alter the HTML output generated by showdown

    For example, if you wanted to change <div class="header"> to be <header>, you could implement an output modifier.

    They have the lowest priority in the subparser order, which means they are called right before the cleanup step and after all other subparsers are called.

Regex and Replace properties

Regex/replace style extensions are very similar to javascript's string.replace function. Two properties are given, regex and replace.

  • regex should be either a string or a RegExp object. Keep in mind that, if a string is used, it will automatically be given a g modifier, that is, it is assumed to be a global replacement.

  • replace can be either a string or a function. If replace is a string, it can use the $1 syntax for group substitution, exactly as if it were making use of string.replace (internally it does this actually)

Example:

var myext = {
  type: 'lang',
  regex: /markdown/g,
  replace: 'showdown'
};

Filter property

Alternately, if you'd just like to do everything yourself, you can specify a filter property. The filter property should be a function that acts as a callback.

There are 3 parameters passed to the filter callback:

  1. text - the source text within showdown's engine.
  2. converter - the full instance of the current showdown's converter object
  3. options - the options used to initialize the converter

Keep in mind that the filter function should return the transformed text. If you don't, it will fail silently and will result in a blank output.

var myext = {
  type: 'lang',
  filter: function (text, converter, options) {
     // ... do stuff to text ...
     return text;
  }
};

Although Filter extensions are more powerful, they have a few pitfalls that you should keep in mind before using them, specially regarding the converter parameter.

Since the converter param passed to the filter function is the fully initialized instance, any change done to it will propagate outside the scope of the filter function, and will remain until a new instance is created. So, it is not advisable to make ANY change to the converter object.

Another important thing is that, if you call the converter recursively, it will, at some point, call your extension itself. This can lead, in some circumstances, to infinite recursion and it's up to you to prevent this. A simple solution is to place some kind of safeguard to disable your extension if it's called more than x times:

var x = 0;
var myext = {
  type: 'lang',
  filter: function (text, converter) {
    if (x < 3) {
      ++x;
      someSubText = converter.makeHtml(someSubText);
    }
  } 
};

Registering the extension

In order for showdown to know which extensions are available, they need to be registered in the showdown global object. This can be accomplished by calling showdown.extension function, where the first parameter is the extension's name and the second is the actual extension.

showdown.extension('myext', myext);

Implementation Concerns

One bit which should be taken into account is maintaining both client-side and server-side compatibility. This can be achieved with a few lines of boilerplate code.

(function (extension) {
  if (typeof showdown !== 'undefined') {
    // global (browser or nodejs global)
    extension(showdown);
  } else if (typeof define === 'function' && define.amd) {
    // AMD
    define(['showdown'], extension);
  } else if (typeof exports === 'object') {
    // Node, CommonJS-like
    module.exports = extension(require('showdown'));
  } else {
    // showdown was not found so we throw
    throw Error('Could not find showdown library');
  }
}(function (showdown) {
  // loading extension into shodown
  showdown.extension('myext', function () {
    var myext = { /* ... actual extension code ... */ };
    return [myext];
  });
}));

In the code above, the extension definition is wrapped in a self-executing function to prevent pollution of the global scope. This has the additional benefit of creating several scope layers that can be useful for interaction between sub-extensions global wise or local wise.

It's also loaded conditionaly, in order to make it compatible with different loading mechanisms (such as browser, CommonJS or AMD).

Gotchas

Showdown does the following escape/normalization:

  • Replaces ¨ (trema) with ¨T
  • Replaces $ (dollar sign) with ¨D
  • Normalizes line endings (\r, \r\n are converted into \n)
  • uses \r as a char placeholder

This happens before language extensions are run, which means that if your extension relies on any of those chars, you have to take this into consideration and make the appropriate modifications.

This only applies to language extensions since these chars are unescaped before output extensions are run.

Testing Extensions

The showdown test runner is setup to automatically test cases for extensions. To add test cases for an extension, create a new folder under ./test/extensions which matches the name of the .js file in ./src/extensions. Place any test cases into the filter using the md/html format and they will automatically be run when tests are run.