Prog blog

How to render markdown in Angular

How to render markdown in Angular

For a week I've been looking for a sensible way to do markdown rendering and I certainly don't recommend ngx-markdown, which is an overlay on mark.js. Such a solution cannot be easily developed later. For example according to Audits guidelines you shouldn't use popular png or jpeg a webp anymore. And here the stairs start, you can't use ordinary img just picture with fallback to png/jpeg, and in ngx-markdown it's not that simple, you have to overwrite the renderer and create a string representing html. This looks stupid in Angular. That's why I dug up on the web and somebody smart created remark, a great markdown parser to AST. Thanks to the tree we are able to create our own markdown renderer in Angular, which we can freely modify and expand.

Example of using remark to create an AST from a markdown document.

import * as links from 'remark-inline-links';
import * as markdownParse from 'remark-parse';
import * as unifed from 'unified';

import { Node } from 'unist';

const body = `
# Heading 1

Some paragraph [google.pl](https://google.pl)

*italic*
**bold**

  `;
const processor = unifed()
  .use(markdownParse)
  .use(links);
const node: Node = processor.runSync(processor.parse(body));

console.log(JSON.stringify(node, null, 2));

Calling this simple script we will get a tree of markers used in markdown in the console, which can be easily converted to html.

{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "children": [
        {
          "type": "text",
          "value": "Heading 1",
          "position": { ... }
        }
      ],
      "position": { ... }
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "text",
          "value": "Some paragraph ",
          "position": { ... }
        },
        {
          "type": "link",
          "title": null,
          "url": "https://google.pl",
          "children": [
            {
              "type": "text",
              "value": "google.pl",
              "position": { ... }
            }
          ],
          "position": { ... }
        }
      ],
      "position": { ... }
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "emphasis",
          "children": [
            {
              "type": "text",
              "value": "italic",
              "position": { ... }
            }
          ],
          "position": { ... }
        },
        {
          "type": "text",
          "value": "\n",
          "position": { ... }
        },
        {
          "type": "strong",
          "children": [
            {
              "type": "text",
              "value": "bold",
              "position": { ... }
            }
          ],
          "position": { ... }
        }
      ],
      "position": { ... }
    }
  ],
  "position": { ... }
}

Now all you have to do is create a simple component that will take node as input, from which it will generate html from the markdown AST node.

node.component.ts

// tslint:disable: component-selector

import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

import { Node } from 'unist';
import { environment } from 'src/environments/environment';

@Component({
  selector: '[node]',
  templateUrl: './node.component.html',
  styleUrls: ['./node.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodeComponent {
  @Input() node: Node;
}

node.component.html

<ng-container *ngFor="let child of node.children">
  <ng-container [ngSwitch]="child.type">
    <b *ngSwitchDefault
      >""""""Missing child type: {{ child.type }}""""""""" {{ child | json }}</b
    >
    <p *ngSwitchCase="'paragraph'" [node]="child"></p>
    <ng-container *ngSwitchCase="'text'">{{ child.value }}</ng-container>
    <em *ngSwitchCase="'emphasis'" [node]="child"></em>
    <ng-container *ngSwitchCase="'heading'">
      <h1 *ngIf="child.depth === 1" [node]="child"></h1>
      <h2 *ngIf="child.depth === 2" [node]="child"></h2>
      <h3 *ngIf="child.depth === 3" [node]="child"></h3>
      <h4 *ngIf="child.depth === 4" [node]="child"></h4>
      <h5 *ngIf="child.depth === 5" [node]="child"></h5>
      <h6 *ngIf="child.depth === 6" [node]="child"></h6>
    </ng-container>
    <img 
      *ngSwitchCase="'image'"
      [src]="child.url"
      [alt]="child?.alt"
      [title]="child?.title" />
    <a
      *ngSwitchCase="'link'"
      [href]="child?.url"
      [title]="child?.title"
      [node]="child"
    ></a>
  </ng-container>
</ng-container>

The solution is currently used on my blog, so you can convert every possible node AST to any component or html tag of your choice.