Rendering Markdown In Angular
By: Lucio Francisco
Tags: JavaScript, Angular, Typescript, Markdown

Rendering Markdown In Angular

How to use Angular directives to render markdown documents

Markdown Renderer in action

There are plenty of third party libraries dealing with markdown documents with marked.js, showdown.js and markdown-it among the most popular, so, we are not here to reinvent the wheel.

However, all of the available libraries generate an HTML output, so, there’s not much you can do with them in Angular rather than passing that output to the innerHTML property of an element within your app with little to no control about the outcome.

How about using standard Angular directives to perform the rendering instead?

Choosing The Library

In order to gain the full control of the rendered output we’ll be needing a library parsing the markdown text in some sort of abstract data format to be fed into our component’s view.

After digging into many of the libraries documentation and source code, I decided to go with remarkjs, part of unifiedjs ecosystem.

As per the project’s main page: “The project parses and compiles Markdown, and let programs process Markdown without ever compiling to HTML”; sounding exactly like what we were searching for.

mdAST

When using remark to parse Markdown documents the library outputs what they call a “Markdown Abstract Syntax Tree” shortened to mdAST and fully documented here.

The data tree has a root node (the document), branches made of block nodes (headings, paragraphs, blockquotes, lists…) and leaves made of phrasing nodes (text, links, inline code, images…).

We’ll be following this very same data model to shape our components around with a MarkdownRoot component for the document, a MarkdownBlock component for the branches and a MarkdownInline component for the leaves to build up our tree view.

Rendering The Data Tree

Once the input text has been parsed into the mdAST, the root component loops on the root node children to render their content. Every data node contains a type value we’ll be using within the ngSwitch directive:

Markdown Block Template

At the root level, the MarkdownRoot component renders the top level blocks instantiating wm-block or wm-inline directives depending on the node type:

blockquote , listItem , leftAligned , centerAligned and rightAligned types render into blocks.

, , , and types render into blocks. heading , paragraph and tableCell types render into inlines.

While types like thematicBreak and code render directly into their final <hr> and <code> elements respectively.

Rendering Blocks

The MarkdownBlock component mimics the very same behaviour of MarkdownRoot. As a matter of fact, both components share the same template since they are rendering their views the exact same way:

Markdown Block and Root components

The component body does very little since most of the magic is done by Angular rendering the view. There are just two helper methods implemented:

toc() returning a string to assign the heading id(s) with in order to support a table of contents.

returning a string to assign the heading id(s) with in order to support a table of contents. pos() computing a line position within the source text for every rendered block.

The MarkdownRoot is a superset of the MarkdownBlock component implemented by extending its class and adding:

parse() to actually parse the input source text into the mdAST data tree.

to actually parse the input source text into the mdAST data tree. notes() returning the array of footnotes to render separately.

MarkdownRoot also implements a navigate output emitting the url string when the user click on a link , so, for the component container to act upon it according to the application logic.

Rendering Inlines

As you can see here below, inline elements render recursively as well:

Markdown Inline Template

In fact, only text , inlineCode and image type of nodes render their value (or url ) while all the other types ( emphasis , strong , link , …) instantiate a child wm-inline component going down the tree instead.

Image Sizes

Gaining control over the rendering gives us the possibility of resizing images as well, something I always wanted to see implemented in markdown.

There are many established practises, though, and for this implementation I decided to go with CSS selectors on special url parameters:

Markdown Inline style

So, basically, in our renderer all the images defaults to 100% width, while by appending a fragment to the url the relevant style is applied:

Appending #25 , #33 , #50 , #66 or #75 resizes the image from 25% up to 75% respectively.

, , , or resizes the image from 25% up to 75% respectively. Appending #icon , #thumb , #small or #regular resizes the image to 48px, 150px, 400px or 1024px fixed size respectively.

This way the size information is kept entirely client side since browsers do not transmit fragments to the server when requesting content. However, the browser can read it and applies the styling accordingly.

Handling Navigation

While rendering links, the component template assigns the node url to the href attribute of an anchor element however, the standard behavior is prevented by the navigate() method called on click :

Markdown Inline Component

The navigate() method simply reverts to the root parent EventEmitter right before returning false stopping the chain of default events.

Parsing The Source Text

The input source text is parsed into the data tree with the help of a MarkdownTree service:

Markdown Tree service

The parse() method relies on remark to parse the input markdown into the mdAST tree fully defined by a set of Typescript interfaces in ‘tree-types.ts’.

Additionally, the service scans the tree for definitions and footnotes among the top nodes for later use by:

definition() , seeks for a definition type of node within the data tree. Definitions node are resources (aka urls) to be referenced elsewhere by an imageReference or a linkReference type of node.

, seeks for a type of node within the data tree. Definitions node are resources (aka urls) to be referenced elsewhere by an or a type of node. footnote() , seeks for a footnoteDefinition type of node to be referenced elsewhere by a footnoteReference type of node.

, seeks for a type of node to be referenced elsewhere by a type of node. footnoteIndex() , returns the numeric index (starting from 1) of the requested footnoteReference .

A text() helper method completes the service returning the plain text resulting by flattening a tree branch.

Requiring Remark

Instead of importing remarkjs, the library of our choice, in the service module as it is I decided to wrap what was needed into a separate reparse.js module:

Reparse.js module

This way, we are actually requiring only the parser module adding the plugins of our choice to support superscript/subscript and block alignment.

Note that remarkjs is designed to work with Node.js relying on toolchain’ shims to work in browsers as well. Since version 6, Angular dropped all the node shims, so, our reparse module applies a minimal process shim when needed.

Supporting Multiple Documents

As you may have noticed, the MarkdownTree service is provided within the MarkdownRoot component scope allowing to have multiple instances of the service across the same application. This is the way we support multiple documents since every instance of MarkdownRoot will have its own instance of the MarkdownTree service shared with its own MarkdownBlock and MarkdownInline children.

Try it Yourself

The MarkdownModule is part of wizdm.io, an open source project hosted on github.

Feel free to reach me out at hello@wizdm.io for whatever question or curiosity.

A fully functional demo, including the full source code, showcasing the markdown rendering is also available on StackBlitz here: