DEV Community

loading...
Cover image for How to Customize Markdown

How to Customize Markdown

Graham Trott
Software Engineering relic with a keen interest in making programming more accessible to ordinary people.
・5 min read

Introduction

Markdown is a lightweight markup language with a plain text formatting syntax. Authors of articles at Dev.to and documentation at GitHub will be familiar with the way it allows styling such as different sized section headers, bold and italic text, hyperlinks and pictures to be specified using simple additions to their text.

Markdown is used to render text. Not just plain text; it knows about headings, hyperlinks, lists, images and more, all of which are specified by special markup sequences in the incoming text. The markup processor transforms this text into HTML, ready for inclusion in the page that's delivered to users.

If you want to include marked-up text in your own pages you'll need a markup processor and a very good example is Showdown.

Although standard markup has a useful range of features there are times when you might want a little more. To take care of those times, Showdown supports custom extensions you can write yourself. This article will explain how to do it.

How does Showdown work?

A markup processor does its job when you want to display marked-up text. How the text got marked up and where it came from is not its concern. It simply takes the source markup and returns you the processed version as HTML, which you can then place into your page, most often by setting the content of a <div>.

Showdown is a JavaScript module that you can download from the GitHub repository linked to above, or you can include it in your document head as

<script src="https://cdn.rawgit.com/showdownjs/showdown/1.9.0/dist/showdown.min.js"></script>

Using it is very simple. Here's the example they provide on GitHub (slightly adapted):

const converter = new showdown.Converter();
const html = converter.makeHtml('# Hello, markdown!');

which when run gives the output

<h1 id="hellomarkdown">Hello, markdown!</h1>

Showdown Extensions

A Showdown extension is code you write yourself and plug into Showdown. After loading the module and before running the code above, create an extension by calling

showdown.extension(`MyExtension`, {
  type: `lang`,
  filter: function (text) {
    return myShowdownExtensionProcessor(text);
  });
}

where myShowdownExtensionProcessor is your code that does whatever it is you need. To initialize the converter we now have

const converter = new showdown.Converter({ extensions: [`MyExtension`] });
const html = converter.makeHtml('# hello, markdown!');

which creates a new Converter with your extension plugged into it. In this example it processes the same text as before.

Creating an extension

There are any number of things you might want to do to extend Showdown, so here's just one example. I have some special features that I want to identify by creating blocks of text bracketed by ~ symbols, inside which I put all the information that tells my extension the transformation I want it to perform.

One such special feature is where text contains code fragments, that I want displayed in a given color and with monospaced font. This can be done by embedding HTML into the markup source but it's clumsy, long-winded and error-prone. I would like to do it like this, where I want to highlight the word switch:

The ~m:switch~ command needs special handling.

Here, m: is a code signalling that the word 'switch' should be displayed in color and with the monospace font.

The second feature is more complicated. My page is a single-page design where I can't use conventional hyperlinks because they simply tell the browser to load a new page. When my special hyperlink is clicked I want to stay on the same page and instead of using the URL in the href attribute I want a data-id attribute to carry the information about what should happen when the link is clicked. I may also want the link id to be different to the text shown. Putting all this together my chosen markdown syntax is:

See ~l:here!In Depth~ for more detail.

Here, l: indicates a hyperlink, and the exclamation mark introduces the actual text of the link when it's different to what follows the l:. That part is optional; if it's not there, the same text will be used for display and as the payload. Here, the text In Depth will be shown as the link text, but when it's clicked, here will be the payload that determines what the page does next.

When the Showdown processor runs, the output HTML generated by my extension will be

<a href="#" data-id="here">In Depth</a>

which has the special attribute data-id carrying the payload.

There are more complex things that we might want to do with links, requiring a more elaborate syntax. To illustrate, let's suppose we need to pass some special information that governs how the transformed text will appear. I'll amend the syntax to

See ~l:here|b!In Depth~ for more detail.

where the |b will tell the renderer to apply bold or maybe load up a different dataset before redrawing the display. The generated HTML will now look like this:

<a href="#" data-id="here|b">In Depth</a>

so the code that processes the click on this link will have to deal with the |b. There's a 'gotcha' - a special case where there's a pipe symbol but no exclamation mark, so the extension processor knows to use the text up to the pipe as the link text.

Wiring it up

All these customized markups are arbitrary. The precise nature of their transformations is likely to change so it's best to keep them somewhere they can be updated easily. Because any given piece of text can contain several transformations, we need to set things up to handle them all. One way to do this is to add a bit more code when we declare the extension, such as:

showdown.extension(`MyExtension`, {
  type: `lang`,
  filter: function (text) {
    return text.replace(/~([^~]+)~/g, function (match, group) {
      return myShowdownExtensionProcessor(group);
    }
  });

This will extract each of the groups - the places where text is contained between 2 tilde characters (~) - and send each one in turn to my custom extension processor, which will decode them and return the transformed results. This means I only have to deal with one special request at a time; the regular expression in the block above takes care of feeding them to you.

The extension processor

You can code this processor however you like. Its job is to do some string processing and deal with what it finds. The parameter group contains just the text between the 2 tildes so it's not a massive task. The processor sends back the transformed text, such as in the examples above. In the case of the links, your code will also trap a mouse click on the link, extract the contents of data-id and do whatever they mean.

Example of usage

The Programmer's Reference for the EasyCoder scripting language contains several hundred pages, each one describing a single command, value or condition in the language. The documentation page is driven by a script that operates in 2 modes, viz. Viewing and Editing, the latter being protected by a password. The editor has a textarea for the description of the item, and this allows markup to be included.

In View mode the script calls its own Showdown module to render the content of the description fields, and the two special features I described above are both used to allow non-standard markup syntax to be included, greatly reducing the amount of typing needed when editing. Processing of the markup text - the groups fed out of the Showdown extension - is done in script, making it very easy to customize. EasyCoder has its own plugin extension that includes the JavaScript code presented above and interfaces it to a special command in the language that invokes the markup decoder. This plugin is documented in the same Programmer's Reference that it powers.

Title photo by Kaitlyn Baker on Unsplash

Discussion (0)