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
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.