Browser…​unplugged
Showcases of modern Browser technologies

August 20, 2017

Shortcodes as custom tags in Markdown

Square brackets are the most-used format for alternative tag-like formats - first popularized by BBCode, then used in WordPress for describing widgets. My new node.js plugin markdown-it-shortcode-tag  uses angle brackets instead. Why divert from the known practise?

Markdown  is a language for writing text in a format that can be automatically converted into pleasantly-styled HTML, but retains the readability of plain text. But there are lots of use cases where technical instructions need to be intermixed with readable content. It is understandable that standard Markdown is weary of these and does support only very few like links and image tags.

If you reach the point where you decide that this standard format isn't enough for your needs and extensions are unavoidable, the question of format arises. You might want to describe a widget, let's say an audio control floated to the right side:

<media type="audio" src="/assets/soundbit.mp3" side="right">

You could argue that the use of square brackets to mark the shortcode is a known format, so why introduce a new one with angle brackets?

For one, the format is not really the same. None of the Markdown-defined uses of brackets includes the use of attributes. The message thus is: if it has attributes, it lies outside the scope of Markdown and therefore has a distinct format.

My reasoning places emphasis on another aspect. If you disturb the basic assumption that Markdown is readable, you should do it in such a way that at least the rendered output is distraction-free. If you process a text with a shortcode with a standard-conforming renderer that does not understand them, graceful degradation should occure.

A syntax with square brackets would lead to the text of the shortcode remaining visible as-is. With angle brackets, both the renderer and a browser would read the shortcode as a HTML5 custom tag. If it is unknown, it simply has no visible representation. That is a much better outcome in my mind.

Use on this site

I like markdown-it  for its support of plugins. On the other hand, Harp , the static site generator I decided to build this site with, uses marked . And that is no help at all if you try to enhance it. To render this site, I have therefore exchanged  the renderer in the terraform  engine that powers Harp. Then I could implement some shortcode tags with my plugin.

Harp defines local variables that the template preprocessors Pug , formerly known as Jade, and EJS  have access to. The content of templates can be inserted anywhere in other templates as so-called partials .

With my modification, both mechanisms can also be utilized in Markdown. One shortcode links to a partial template (here, a file called media.jade inserting an image as a left float):

<partial src="partials/media" medium="avatar" side="left" width="300px">

The other renders the content of a local variable (title):

<local title>

The definition of the shortcodes had to be added to the terraform Markdown renderer  like this:

// load markdown parser
var md = require('markdown-it')({ html: true, linkify: true });

// define shortcodes
const shortcodes = {
  partial: { // includes a template
    render: function (attrs, env) {
      const path = attrs.src;
      if (!path) throw new TerraformError(/*...*/);
      delete attrs.src;

      // function env.partial() is provided by terraform and
      // returns the rendered template. Attributes are made
      // available to the template as local variables.
      return env.partial(path, attrs) || '';
    }
  },

  local: { // returns a variable
    inline: true,
    render: function (attrs, env) {
      // fetch the first attribute name
      const name = Object.getOwnPropertyNames(attrs)[0];
      if (!name || locals[name] !== true) throw new TerraformError(/*...*/);

      // get the variable content from env
      return env[name];
    }
  }
};

// activate plugin
md.use(require('markdown-it-shortcode-tag'), shortcodes);

// terraform interface
module.exports = function(fileContents, options){

  return {
    compile: function(){
      return function (locals) {
        // call markdown-it and supply the local environment
        return md.render(fileContents.toString(), locals);
      };
    }, //...
  };

};