{"id":13808706,"url":"https://github.com/ericleib/ngx-remark","last_synced_at":"2026-03-02T11:01:59.157Z","repository":{"id":181984042,"uuid":"667756300","full_name":"ericleib/ngx-remark","owner":"ericleib","description":"Render markdown with custom Angular templates","archived":false,"fork":false,"pushed_at":"2025-12-04T16:14:04.000Z","size":879,"stargazers_count":13,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-08T00:28:58.008Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://ericleib.github.io/ngx-remark/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ericleib.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-07-18T08:28:19.000Z","updated_at":"2025-12-04T16:14:08.000Z","dependencies_parsed_at":"2024-11-19T00:32:47.701Z","dependency_job_id":"26b20740-f4e3-487f-b9d6-82eb1411f2cf","html_url":"https://github.com/ericleib/ngx-remark","commit_stats":null,"previous_names":["ericleib/ngx-remark"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/ericleib/ngx-remark","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericleib%2Fngx-remark","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericleib%2Fngx-remark/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericleib%2Fngx-remark/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericleib%2Fngx-remark/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ericleib","download_url":"https://codeload.github.com/ericleib/ngx-remark/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericleib%2Fngx-remark/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29999221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-02T09:59:02.300Z","status":"ssl_error","status_checked_at":"2026-03-02T09:59:02.001Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T01:01:49.869Z","updated_at":"2026-03-02T11:01:59.145Z","avatar_url":"https://github.com/ericleib.png","language":"TypeScript","funding_links":[],"categories":["Third Party Components"],"sub_categories":["Markdown"],"readme":"[![Build project](https://github.com/ericleib/ngx-remark/actions/workflows/build.yml/badge.svg)](https://github.com/ericleib/ngx-remark/actions/workflows/build.yml)\n[![npm version](https://badge.fury.io/js/ngx-remark.svg)](https://badge.fury.io/js/ngx-remark)\n\n# ngx-remark\n\n**ngx-remark** is a lightweight library that renders Markdown using **native Angular components and templates**.\n\nMost 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.\n\n**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 `\u003cremark\u003e` component ships with clean default templates for every standard Markdown element, but **you can override any of them** with your own components.\n\nTypical use cases include:\n\n- Rendering code blocks with a full-featured editor (e.g. Monaco, CodeMirror)\n- Adding interactive tooltips, popovers, or badges on specific elements\n- Turning links or buttons into Angular-powered actions (routing, modals, etc.)\n- Deep integration with Angular features like the [`Router`](#router-integration), forms, or signals\n\n## Demo\n\n- [Playground](https://ericleib.github.io/ngx-remark/)\n- [Stackblitz](https://stackblitz.com/edit/stackblitz-starters-gah83vcs?file=src%2Fmain.ts)\n\n## Installation\n\nInstall the library with npm:\n\n```bash\nnpm install ngx-remark remark\n```\n\n\n## Importing the library\n\nImport `RemarkModule` in your standalone component or module:\n\n```typescript\nimport { RemarkModule } from 'ngx-remark';\n\n@Component({\n  selector: 'app-root',\n  templateUrl: './app.component.html',\n  imports: [RemarkModule],\n})\nexport class App { }\n```\n\nNote: `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.\n\n## Usage\n\nUse the `\u003cremark\u003e` component to render Markdown:\n\n```html\n\u003cremark markdown=\"# Hello World\"/\u003e\n```\n\nThe above renders the HTML will all default templates.\n\nYou can customize the Remark processing pipeline with the optional `processor` input (the default is `unified().use(remarkParse)`):\n\n```html\n\u003cremark [markdown]=\"markdown\" [processor]=\"processor\"\u003e\u003c/remark\u003e\n```\n\nAs an example, the following uses the [remark-gfm](https://github.com/remarkjs/remark-gfm) plugin to support GitHub Flavored Markdown:\n\n```typescript\nimport remarkGfm from 'remark-gfm';\nimport remarkParse from 'remark-parse';\nimport { unified } from 'unified';\n\nprocessor = unified().use(remarkParse).use(remarkGfm);\n```\n\nYou can override the templates for any node type with the `\u003cng-template\u003e` element and the `remarkTemplate` directive:\n\n```html\n\u003cremark markdown=\"# Hello World\"\u003e\n\n  \u003cng-template remarkTemplate=\"heading\" let-node\u003e\n    \u003ch1 *ngIf=\"node.depth === 1\" [remarkNode]=\"node\" style=\"color: red;\"\u003e\u003c/h1\u003e\n    \u003ch2 *ngIf=\"node.depth === 2\" [remarkNode]=\"node\"\u003e\u003c/h2\u003e\n    ...\n  \u003c/ng-template\u003e\n\n\u003c/remark\u003e\n```\n\nIn the above example, note the following:\n\n- The headings of depth 1 are customized with a red color.\n- The `remarkTemplate` attribute must be set to the name of the [MDAST](https://github.com/syntax-tree/mdast) node type.\n- 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.\n- Since the heading node might have children (in particular `text` nodes), the `remarkNode` directive is used to render the children of the node.\n\nIt is possible to use the structural shorthand syntax for the `remarkTemplate` directive:\n\n```html\n\u003cremark markdown=\"This is a paragraph with [link](https://example.com)\"\u003e\n\n  \u003cspan *remarkTemplate=\"'link'; let node\" style=\"color: green;\"\u003e\n    This is a link: \u003ca [href]=\"node?.url\" [title]=\"node.title ?? ''\" [remarkNode]=\"node\"\u003e\u003c/a\u003e\n  \u003c/span\u003e\n\n\u003c/remark\u003e\n```\n\nIf the node type doesn't have children, the `[remarkNode]` directive isn't required:\n\n```html\n\u003cremark [markdown]=\"markdownWithCodeBlocks\"\u003e\n\n  \u003cmy-code-editor *remarkTemplate=\"'code'; let node\"\n    [code]=\"node.value\"\n    [language]=\"node.lang\"\u003e\n  \u003c/my-code-editor\u003e\n\n\u003c/remark\u003e\n```\n\nYou can customize various node types by adding as many templates as needed:\n\n```html\n\u003cremark [markdown]=\"markdownWithCodeBlocks\"\u003e\n\n  \u003cmy-code-editor *remarkTemplate=\"'code'; let node\"\n    [code]=\"node.value\"\n    [language]=\"node.lang\"\u003e\n  \u003c/my-code-editor\u003e\n\n  \u003cspan *remarkTemplate=\"'link'; let node\" style=\"color: green;\"\u003e\n    This is a link: \u003ca [href]=\"node?.url\" [title]=\"node.title ?? ''\" [remarkNode]=\"node\"\u003e\u003c/a\u003e\n  \u003c/span\u003e\n\n\u003c/remark\u003e\n```\n\n## Router integration\n\nBy default, links in the Markdown document are rendered as non-Angular links, ie.:\n\n```html\n\u003ca href=\"https://google.com\"\u003e\u003c/a\u003e\n```\n\nA common problem is handling links that point to routes in the Angular application. This is a good use-case for ngx-remark:\n\n```html\n\u003cremark [markdown]=\"markdown\"\u003e\n  \u003cng-template [remarkTemplate]=\"'link'\" let-node\u003e\n    @if(node.url.startsWith('https://')) {\n      \u003ca [href]=\"node.url\" target=\"_blank\" [title]=\"node.title ?? ''\" [remarkNode]=\"node\"\u003e\u003c/a\u003e\n    }\n    @else {\n      \u003ca [routerLink]=\"node.url\" [title]=\"node.title ?? ''\" [remarkNode]=\"node\"\u003e\u003c/a\u003e\n    }\n  \u003c/ng-template\u003e\n\u003c/remark\u003e\n```\n\nNote that we handle 2 types of links:\n- External links (starting with `https://`) are rendered with `href` and `target=\"_blank\"`.\n- Internal links use the Angular router\n\n(In practice, the distinction between the 2 types might be more subtle)\n\n## Cursor symbol\n\nWhen 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.\n\nThis can be achieved in 3 steps:\n\n1. Create a plugin to insert a custom node after the last `text` node in the AST:\n\n```ts\nprocessor.use(() =\u003e this.placeholderPlugin);\n\nplaceholderPlugin = (tree: Node) =\u003e {\n  visit(tree, \"text\", (node: Text, index: number, parent: Parent) =\u003e {\n    parent.children.push({type: \"cursor\"} as any);\n    return EXIT;\n  }, true);\n  return tree;\n}\n```\n\n2. Add a template to render this component:\n\n```html\n\u003cspan *remarkTemplate=\"'cursor'\" [ngClass]=\"{cursor: streaming}\"\u003e\u003c/span\u003e\n```\n\n3. Add styles to the `.cursor` class:\n\n```css\n.cursor {\n  display: inline-block;\n  height: 1rem;\n  vertical-align: text-bottom;\n  width: 10px;\n  animation: cursor-glow 0.3s ease-in-out infinite;\n  background: grey;\n}\n\n@keyframes cursor-glow {\n  50% {\n    opacity: .2;\n  }\n}\n```\n\n## Plugins\n\n### Remark plugins\n\nRemark is a tool that transforms markdown with [plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins).\n\nFor example, converting gemoji shortcodes into emoji can be achieved with the [remark-gemoji](https://github.com/remarkjs/remark-gemoji) plugin:\n\n```typescript\nimport { unified } from 'unified';\nimport remarkParse from 'remark-parse';\nimport remarkGemoji from 'remark-gemoji';\n\nprocessor = unified()\n  .use(remarkParse)\n  .use(remarkGemoji);\n```\n\nThis 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).\n\nOther 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.\n\n\n### Code highlighting\n\nSyntax highlighting for code blocks can be enabled by adding [Prismjs](https://prismjs.com/) to your project.\n\n#### Install Prism\n\nThe simplest way to install Prism is by loading stylesheets and scripts from a CDN, for example:\n\n```html\n\u003chead\u003e\n  ...\n  \u003clink href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism-okaidia.min.css\" rel=\"stylesheet\" /\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003capp-root\u003e\u003c/app-root\u003e\n  \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n```\n\n#### Render code blocks\n\nWith Prism globally loaded, you can add the `remark-prism` component as a template inside your `remark` component:\n\n```ts\nimport { PrismComponent, RemarkModule } from 'ngx-remark';\n\n@Component({\n  ...\n  imports: [..., RemarkModule, PrismComponent]\n})\n```\n\n```html\n\u003cremark [markdown]=\"markdown\"\u003e\n  \u003cremark-prism *remarkTemplate=\"'code'; let node\"\n    [code]=\"node.value\"\n    [language]=\"node.lang\"\u003e\n  \u003c/remark-prism\u003e\n\u003c/remark\u003e\n```\n\nYou can also nest `remark-prism` inside a more complex component or template (eg. including a \"Copy to clipboard\" button).\n\nNote that there is no need to customize the `processor`, as the component takes the raw code as an input.\n\n#### Advanced setup\n\nYou may also install Prism with `npm i prismjs` and add the scripts and stylesheets to your `angular.json` file.\n\nYou may want to avoid to avoid loading the stylesheet globally, as it will add styling to any `\u003ccode\u003e` element in your app. One way to scope the styling only to this library is use this trick in your SCSS stylesheet:\n\n```scss\n@use \"sass:meta\";\n\nremark-prism {\n  @include meta.load-css(\"node_modules/prismjs/themes/prism-okaidia.css\");\n}\n```\n\nIf 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:\n\n```json\n{\n  \"input\": \"node_modules/prismjs/components/\",\n  \"glob\": \"prism-*.min.js\",\n  \"output\": \"components/\"\n}\n```\n\nThis will add all the ~300 language files to your assets so they can be loaded when needed.\n\n### Headings with anchor links\n\nYou can render headings with an anchor link with the provided component `remark-heading`:\n\n```ts\nimport { RemarkModule, HeadingComponent } from 'ngx-remark';\n\n@Component({\n  ...\n  imports: [..., RemarkModule, HeadingComponent]\n})\n```\n\nand\n\n```html\n\u003cremark [markdown]=\"markdown\"\u003e\n  \u003cremark-heading *remarkTemplate=\"'heading'; let node\" [node]=\"node\"\u003e\u003c/remark-heading\u003e\n\u003c/remark\u003e\n```\n\nThe `id` is automatically generated from the heading text content.\n\n### Math expressions\n\nMath expressions can be rendered with [KaTeX](https://katex.org/).\n\nThis requires four steps:\n- Install KaTeX in your project. Either:\n  - Load it from a CDN.\n  - 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).\n- Import the KaTeX stylesheet into your styles (or simply load it from a CDN)\n- Add the `remark-math` plugin to your processor with `processor.use(remarkMath)`\n- Render math expressions with the provided `remark-katex` component:\n\n```ts\nimport { RemarkModule, KatexComponent } from 'ngx-remark';\n\n@Component({\n  ...\n  imports: [RemarkModule, KatexComponent]\n})\n```\n\n```html\n\u003cremark [markdown]=\"markdown\" [processor]=\"processor\"\u003e\n  \u003cremark-katex *remarkTemplate=\"'math'; let node\" [expr]=\"node.value\"\u003e\u003c/remark-katex\u003e\n  \u003cremark-katex *remarkTemplate=\"'inlineMath'; let node\" [expr]=\"node.value\"\u003e\u003c/remark-katex\u003e\n\u003c/remark\u003e\n```\n\nIn the example above, we make no difference between `math` and `inlineMath` elements, but in practice they might require minor styling differences.\n\n### Mermaid diagrams\n\n[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.\n\n#### Install Mermaid\n\nThe simplest way to install Mermaid is to load the library from a CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js\"\u003e\u003c/script\u003e\n```\n\n#### Render mermaid code blocks\n\nAdd 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:\n\n```html\n\u003cremark [markdown]=\"markdown\"\u003e\n  \u003cng-template [remarkTemplate]=\"'code'\" let-node\u003e\n    @if(node.lang === 'mermaid') {\n      \u003cremark-mermaid [code]=\"node.value\"/\u003e\n    }\n    @else {\n      \u003cremark-prism [code]=\"node.value\" [language]=\"node.lang\"/\u003e\n    }\n  \u003c/ng-template\u003e\n\u003c/remark\u003e\n```\n\nNote that there is no need to customize the `processor`, as the component takes the raw mermaid code as an input.\n\n## Custom Markdown syntax\n\nRendering custom Markdown syntax requires 2 steps:\n- 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`). \n- Rendering the custom nodes with an Angular template, such as: `\u003cspan *remarkTemplate=\"'my-type'; let node\"\u003e{{node.value}}\u003c/span\u003e`.\n\n### Example\n\nWe want to support a custom block such as:\n\n```\n:::dropdown  \nOption 1\nOption 2\nOption 3  \n:::\n```\n\nFirst, we create a custom processor that finds this syntax within the AST:\n\n```typescript\nprocessor = unified()\n  .use(remarkParse)\n  .use(() =\u003e this.plugin);\n\nplugin = (tree: Node) =\u003e {\n  visit(tree, 'paragraph', (node: Paragraph, index: number, parent: Parent) =\u003e {\n    const firstChild = node.children[0];\n    const lastChild = node.children.at(-1)!;\n    if (\n      firstChild.type === 'text' \u0026\u0026\n      lastChild.type === 'text' \u0026\u0026\n      firstChild.value.startsWith(':::dropdown') \u0026\u0026\n      lastChild.value.trimEnd().endsWith(':::')\n    ) {\n      parent.children[index] = {\n        type: 'dropdown',\n        options: node.children\n          .flatMap((child) =\u003e\n            (child as Text).value\n              .replace(':::dropdown', '')\n              .replace(':::', '')\n              .split('\\n')\n          )\n          .filter((c) =\u003e c),\n      } as any;\n    }\n    return CONTINUE;\n  });\n};\n```\n\nThis custom processor searches for and replaces nodes such as:\n\n```json\n{\n  \"type\": \"paragraph\",\n  \"children\": [\n    {\n      \"type\": \"text\",\n      \"value\": \":::dropdown\\nOption 1\\nOption 2\\nOption 3\\n:::\"\n    }\n  ]\n}\n```\n\nwith:\n\n```json\n{\n  \"type\": \"dropdown\",\n  \"options\": [\n    \"Option 1\",\n    \"Option 2\",\n    \"Option 3\"\n  ]\n}\n```\n\nThen, in our Angular application we can render the dropdown with:\n\n```html\n\u003cremark [markdown]=\"markdown\" [processor]=\"processor\"\u003e\n  \u003cng-template [remarkTemplate]=\"'dropdown'\" let-node\u003e\n    \u003cselect\u003e\n      @for(option of node.options; track $index) {\n      \u003coption\u003e{{ option }}\u003c/option\u003e\n      }\n    \u003c/select\u003e\n  \u003c/ng-template\u003e\n\u003c/remark\u003e\n```\n\nHere's a working example: https://stackblitz.com/edit/stackblitz-starters-emzbbhj4?file=src%2Fmain.ts\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericleib%2Fngx-remark","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericleib%2Fngx-remark","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericleib%2Fngx-remark/lists"}