https://github.com/ericleib/ngx-remark
Render markdown with custom Angular templates
https://github.com/ericleib/ngx-remark
Last synced: 7 days ago
JSON representation
Render markdown with custom Angular templates
- Host: GitHub
- URL: https://github.com/ericleib/ngx-remark
- Owner: ericleib
- License: mit
- Created: 2023-07-18T08:28:19.000Z (over 2 years ago)
- Default Branch: master
- Last Pushed: 2025-12-04T16:14:04.000Z (3 months ago)
- Last Synced: 2025-12-08T00:28:58.008Z (3 months ago)
- Language: TypeScript
- Homepage: https://ericleib.github.io/ngx-remark/
- Size: 858 KB
- Stars: 13
- Watchers: 1
- Forks: 5
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-angular - ngx-remark - Render markdown with custom Angular templates. (Third Party Components / Markdown)
- awesome-angular - ngx-remark - Render markdown with custom Angular templates. (Third Party Components / Markdown)
README
[](https://github.com/ericleib/ngx-remark/actions/workflows/build.yml)
[](https://badge.fury.io/js/ngx-remark)
# ngx-remark
**ngx-remark** is a lightweight library that renders Markdown using **native Angular components and templates**.
Most Markdown libraries for Angular convert the source to HTML and then bind it with `[innerHTML]`. The problem with this approach is that the resulting HTML lives outside Angular's component tree — you cannot use Angular components, directives, pipes, or dependency injection inside it.
**ngx-remark** takes a different route. It uses [Remark](https://remark.js.org/) to parse Markdown into an AST, then renders that tree with real Angular templates. The `` component ships with clean default templates for every standard Markdown element, but **you can override any of them** with your own components.
Typical use cases include:
- Rendering code blocks with a full-featured editor (e.g. Monaco, CodeMirror)
- Adding interactive tooltips, popovers, or badges on specific elements
- Turning links or buttons into Angular-powered actions (routing, modals, etc.)
- Deep integration with Angular features like the [`Router`](#router-integration), forms, or signals
## Demo
- [Playground](https://ericleib.github.io/ngx-remark/)
- [Stackblitz](https://stackblitz.com/edit/stackblitz-starters-gah83vcs?file=src%2Fmain.ts)
## Installation
Install the library with npm:
```bash
npm install ngx-remark remark
```
## Importing the library
Import `RemarkModule` in your standalone component or module:
```typescript
import { RemarkModule } from 'ngx-remark';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
imports: [RemarkModule],
})
export class App { }
```
Note: `RemarkModule` is a bundle of `RemarkComponent`, `RemarkNodeComponent` and `RemarkTemplateDirective`. You may also import these components individually, but in most cases you will need the three of them together.
## Usage
Use the `` component to render Markdown:
```html
```
The above renders the HTML will all default templates.
You can customize the Remark processing pipeline with the optional `processor` input (the default is `unified().use(remarkParse)`):
```html
```
As an example, the following uses the [remark-gfm](https://github.com/remarkjs/remark-gfm) plugin to support GitHub Flavored Markdown:
```typescript
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import { unified } from 'unified';
processor = unified().use(remarkParse).use(remarkGfm);
```
You can override the templates for any node type with the `` element and the `remarkTemplate` directive:
```html
...
```
In the above example, note the following:
- The headings of depth 1 are customized with a red color.
- The `remarkTemplate` attribute must be set to the name of the [MDAST](https://github.com/syntax-tree/mdast) node type.
- The `let-node` attribute is required to make the `node` variable available in the template. The `node` variable is of type `Node` and can be used to access the properties of the node.
- Since the heading node might have children (in particular `text` nodes), the `remarkNode` directive is used to render the children of the node.
It is possible to use the structural shorthand syntax for the `remarkTemplate` directive:
```html
```
If the node type doesn't have children, the `[remarkNode]` directive isn't required:
```html
```
You can customize various node types by adding as many templates as needed:
```html
```
## Router integration
By default, links in the Markdown document are rendered as non-Angular links, ie.:
A common problem is handling links that point to routes in the Angular application. This is a good use-case for ngx-remark:
```html
@if(node.url.startsWith('https://')) {
}
@else {
}
```
Note that we handle 2 types of links:
- External links (starting with `https://`) are rendered with `href` and `target="_blank"`.
- Internal links use the Angular router
(In practice, the distinction between the 2 types might be more subtle)
## Cursor symbol
When this component is used to display the output of an LLM in streaming mode, it can be a nice touch to insert a glowing "cursor" symbol at the end of the text.
This can be achieved in 3 steps:
1. Create a plugin to insert a custom node after the last `text` node in the AST:
```ts
processor.use(() => this.placeholderPlugin);
placeholderPlugin = (tree: Node) => {
visit(tree, "text", (node: Text, index: number, parent: Parent) => {
parent.children.push({type: "cursor"} as any);
return EXIT;
}, true);
return tree;
}
```
2. Add a template to render this component:
```html
```
3. Add styles to the `.cursor` class:
```css
.cursor {
display: inline-block;
height: 1rem;
vertical-align: text-bottom;
width: 10px;
animation: cursor-glow 0.3s ease-in-out infinite;
background: grey;
}
@keyframes cursor-glow {
50% {
opacity: .2;
}
}
```
## Plugins
### Remark plugins
Remark is a tool that transforms markdown with [plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins).
For example, converting gemoji shortcodes into emoji can be achieved with the [remark-gemoji](https://github.com/remarkjs/remark-gemoji) plugin:
```typescript
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGemoji from 'remark-gemoji';
processor = unified()
.use(remarkParse)
.use(remarkGemoji);
```
This particular plugin works out-of-the-box because it does not introduce a new node type in the syntax tree (emojis are just UTF-8 characters).
Other plugins (such as [remark-directive](https://github.com/remarkjs/remark-directive) or [remark-sectionize](https://github.com/jake-low/remark-sectionize)) may introduce node types that must be rendered with an Angular template or component.
### Code highlighting
Syntax highlighting for code blocks can be enabled by adding [Prismjs](https://prismjs.com/) to your project.
#### Install Prism
The simplest way to install Prism is by loading stylesheets and scripts from a CDN, for example:
```html
...
```
#### Render code blocks
With Prism globally loaded, you can add the `remark-prism` component as a template inside your `remark` component:
```ts
import { PrismComponent, RemarkModule } from 'ngx-remark';
@Component({
...
imports: [..., RemarkModule, PrismComponent]
})
```
```html
```
You can also nest `remark-prism` inside a more complex component or template (eg. including a "Copy to clipboard" button).
Note that there is no need to customize the `processor`, as the component takes the raw code as an input.
#### Advanced setup
You may also install Prism with `npm i prismjs` and add the scripts and stylesheets to your `angular.json` file.
You may want to avoid to avoid loading the stylesheet globally, as it will add styling to any `` element in your app. One way to scope the styling only to this library is use this trick in your SCSS stylesheet:
```scss
@use "sass:meta";
remark-prism {
@include meta.load-css("node_modules/prismjs/themes/prism-okaidia.css");
}
```
If you need the autoloader plugin to work in this context, you will need to add the languages files to your assets with a glob such as:
```json
{
"input": "node_modules/prismjs/components/",
"glob": "prism-*.min.js",
"output": "components/"
}
```
This will add all the ~300 language files to your assets so they can be loaded when needed.
### Headings with anchor links
You can render headings with an anchor link with the provided component `remark-heading`:
```ts
import { RemarkModule, HeadingComponent } from 'ngx-remark';
@Component({
...
imports: [..., RemarkModule, HeadingComponent]
})
```
and
```html
```
The `id` is automatically generated from the heading text content.
### Math expressions
Math expressions can be rendered with [KaTeX](https://katex.org/).
This requires four steps:
- Install KaTeX in your project. Either:
- Load it from a CDN.
- Install it with `npm i katex remark-math` and make it available globally with `(window as any)['katex'] = katex;` (note: the ngx-remark library does not import the katex module to avoid making it a hard dependency).
- Import the KaTeX stylesheet into your styles (or simply load it from a CDN)
- Add the `remark-math` plugin to your processor with `processor.use(remarkMath)`
- Render math expressions with the provided `remark-katex` component:
```ts
import { RemarkModule, KatexComponent } from 'ngx-remark';
@Component({
...
imports: [RemarkModule, KatexComponent]
})
```
```html
```
In the example above, we make no difference between `math` and `inlineMath` elements, but in practice they might require minor styling differences.
### Mermaid diagrams
[Mermaid](https://mermaid.js.org) is a JavaScript based diagramming and charting tool that uses Markdown-inspired text definitions and a renderer to create and modify complex diagrams.
#### Install Mermaid
The simplest way to install Mermaid is to load the library from a CDN:
```html
```
#### Render mermaid code blocks
Add the provided `remark-mermaid` component inside your `remark` component. Mermaid diagrams are typically rendered within code blocks, so your Angular template might look like this:
```html
@if(node.lang === 'mermaid') {
}
@else {
}
```
Note that there is no need to customize the `processor`, as the component takes the raw mermaid code as an input.
## Custom Markdown syntax
Rendering custom Markdown syntax requires 2 steps:
- Parsing the Markdown with a custom [Remark](https://remark.js.org/) processor. The abstract syntax tree (AST) will contain nodes with a custom type (let's say `my-type`).
- Rendering the custom nodes with an Angular template, such as: `{{node.value}}`.
### Example
We want to support a custom block such as:
```
:::dropdown
Option 1
Option 2
Option 3
:::
```
First, we create a custom processor that finds this syntax within the AST:
```typescript
processor = unified()
.use(remarkParse)
.use(() => this.plugin);
plugin = (tree: Node) => {
visit(tree, 'paragraph', (node: Paragraph, index: number, parent: Parent) => {
const firstChild = node.children[0];
const lastChild = node.children.at(-1)!;
if (
firstChild.type === 'text' &&
lastChild.type === 'text' &&
firstChild.value.startsWith(':::dropdown') &&
lastChild.value.trimEnd().endsWith(':::')
) {
parent.children[index] = {
type: 'dropdown',
options: node.children
.flatMap((child) =>
(child as Text).value
.replace(':::dropdown', '')
.replace(':::', '')
.split('\n')
)
.filter((c) => c),
} as any;
}
return CONTINUE;
});
};
```
This custom processor searches for and replaces nodes such as:
```json
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": ":::dropdown\nOption 1\nOption 2\nOption 3\n:::"
}
]
}
```
with:
```json
{
"type": "dropdown",
"options": [
"Option 1",
"Option 2",
"Option 3"
]
}
```
Then, in our Angular application we can render the dropdown with:
```html
@for(option of node.options; track $index) {
{{ option }}
}
```
Here's a working example: https://stackblitz.com/edit/stackblitz-starters-emzbbhj4?file=src%2Fmain.ts