FE-001: Replace custom MarkdownComponent with ngx-markdown
Problem Statement
The Specification Management frontend renders Markdown content (spec bodies, tool descriptions, module contents) using a hand-rolled 94-line MarkdownComponent that wraps marked v18 directly. This custom implementation has the following deficiencies:
- No syntax highlighting — code blocks render as plain monospace text on a dark background with no language-specific token colouring, making spec enforcement examples hard to read.
- No copy-to-clipboard — developers cannot quickly copy code snippets from rendered specs.
- Unsanitised innerHTML — the component sets
innerHTMLdirectly without invokingDomSanitizer, creating a potential XSS vector if spec content is ever sourced from untrusted input. - No extensibility path — adding features (KaTeX for spec formulas, Mermaid for architecture diagrams, emoji shortcodes) requires re-implementing each integration manually.
Solution
Replace the custom MarkdownComponent with ngx-markdown (by jfcere), which provides:
marked-based parsing (already the project's parser) with Angular component/directive/pipe integration- Optional Prism.js syntax highlighting for code blocks
- Optional Clipboard.js one-click code block copying
- Built-in HTML sanitisation via Angular's
DomSanitizer - Optional KaTeX, Mermaid, and Emoji-Toolkit plugin support
MarkdownServicefor programmatic parsing
The custom MkDocs-style tab preprocessor (=== "Tab Name" syntax) will be preserved as a marked extension or pre-processing step, since ngx-markdown does not natively support this syntax.
User Stories
- As a developer viewing a spec detail page, I want code blocks to display with language-specific syntax highlighting, so that I can quickly identify keywords, strings, and structure in enforcement examples.
- As a developer viewing a spec detail page, I want a "Copy" button on code blocks, so that I can paste enforcement configuration into my project without manually selecting text.
- As a developer viewing tool descriptions, I want rendered Markdown to be sanitised, so that I am not exposed to XSS risks from spec content.
- As a developer viewing spec content, I want the tab preprocessor (
=== "Tab Name"syntax) to continue working, so that existing MkDocs-style tabbed content is not broken. - As a developer viewing module contents, I want Markdown to render identically to the current output, so that the migration is invisible to end users.
- As a developer, I want the Markdown rendering to be handled by a single maintained library, so that I do not need to maintain custom parsing glue code.
- As a developer, I want syntax highlighting themes to match the existing dark code block styling (
#1e293bbackground), so that the visual design remains consistent. - As a developer, I want the option to enable KaTeX rendering in the future, so that spec documents can include mathematical expressions for complexity analysis.
- As a developer, I want the option to enable Mermaid diagram rendering in the future, so that spec documents can include architecture diagrams inline.
- As a developer, I want the Markdown component API to remain
[content]="string", so that all three consumer pages (spec-detail, tools, modules) require minimal changes. - As a developer, I want the migration to be a single atomic change, so that there is no intermediate state where some pages use the old component and others use the new one.
- As a developer, I want the old custom
MarkdownComponentfile to be deleted after migration, so that there is no dead code lingering in the codebase.
Implementation Decisions
1. Dependency changes
- Add:
ngx-markdown(latest stable, currently v21.x for Angular 22),marked(already present, will be peer-managed by ngx-markdown),prismjs(for syntax highlighting) - Remove: direct
markedimport in the custom component (ngx-markdown wraps it) - Keep:
markedinpackage.jsonas a peer dependency of ngx-markdown
2. Module configuration
Provide MarkdownModule (or provideMarkdown() for standalone apps) in the application root. Configure:
markedOptions: set{ breaks: true, gfm: true }(matching current behaviour)markedExtensions: register a custom tab preprocessor extension that handles the=== "Tab Name"MkDocs-style syntax
3. Tab preprocessor migration
The current parseTabs() / renderTabsHtml() / preprocess() functions will be converted into a marked extension that:
- Registers a custom tokeniser for lines matching
=== "Tab Name"followed by 4-space indented content - Emits custom HTML blocks (
<div class="md-tabs">...) during the render phase - Is registered via
MARKED_EXTENSIONSinjection token in the provider
This keeps the tab syntax working without modifying ngx-markdown internals.
4. Syntax highlighting approach
Use Prism.js with ngx-markdown's built-in integration:
- Load Prism core + CSS theme in
angular.jsonscripts/styles arrays - Configure language components for the languages used in specs:
typescript,java,xml,yaml,bash,json,groovy - Use a dark theme that matches the existing
#1e293bbackground (e.g.prism-okaidiaorprism-tomorrow)
5. Component replacement strategy
Replace the custom <app-markdown> selector usage with ngx-markdown's <markdown> component (or keep the app-markdown selector by wrapping ngx-markdown's directive). The chosen approach:
- Rewrite
src/app/components/markdown/markdown.tsto be a thin wrapper around ngx-markdown'sMarkdownDirectivewith theapp-markdownselector preserved - This avoids changing the template syntax in all 3 consumer pages — they continue using
<app-markdown [content]="..."> - The wrapper component handles the tab preprocessor as a
markedOptionsextension
6. Consumer pages affected
| Page | Current usage | Change required |
|---|---|---|
spec-detail.ts |
<app-markdown [content]="spec()!.body \|\| ''"> |
Import update only |
tools.ts |
<app-markdown [content]="getTabContent()"> |
Import update only |
modules.ts |
<app-markdown [content]="mod.contents"> |
Import update only |
All three pages import MarkdownComponent from ../../components/markdown/markdown. After the rewrite, they continue importing from the same path with no template changes.
7. CSS migration
The existing markdown.css styles will be reviewed:
- Keep:
.md-tabs,.md-tab,.md-tab-labelstyles (custom tab UI) - Remove:
.markdown-body pre,.markdown-body codebase styles that duplicate what Prism's theme provides - Keep:
.markdown-body h1–h4,p,ul,ol,li,blockquote,tablestyles (ngx-markdown outputs.markdown-bodyclass by default) - Add: Prism theme CSS import (via
angular.jsonor global styles)
8. Sanitisation
ngx-markdown uses Angular's DomSanitizer internally. The custom wrapper will NOT call bypassSecurityTrustHtml() — all sanitisation is delegated to ngx-markdown. Spec content is trusted (from local SQLite via Tauri), but the sanitisation provides defense-in-depth.
Testing Decisions
What constitutes a good test
- Verify that the rendered HTML output for a given Markdown input matches expected structure (element types, class names)
- Verify that code blocks receive Prism.js token spans (syntax highlighting applied)
- Verify that the tab preprocessor produces correct
<div class="md-tabs">structure - Verify that all 3 consumer pages render without errors
Testing approach
Since the project currently has zero test files (no .spec.ts or .stories.ts found), and introducing a full test framework is out of scope for this change:
- Manual verification: Render a sample spec with headings, code blocks (multiple languages), tabs, tables, blockquotes, and lists — confirm visual parity with current output
- Smoke test: Ensure
ng buildcompletes without errors and the app launches in Tauri without console errors - Optional: Add a single
markdown.component.spec.tsthat assertsMarkdownService.parse()produces expected HTML for a known input (low effort, high value)
Prior art
No existing tests in the codebase. This migration is an opportunity to establish the first component test.
Out of Scope
- Adding KaTeX or Mermaid support (future enhancement, not part of this migration)
- Adding a test framework to the project (separate initiative)
- Changing the tab syntax or adding new Markdown extensions beyond what exists today
- Migrating to a different parser (e.g. markdown-it, remark) — staying with marked via ngx-markdown
- Changing the visual design/styling of rendered content beyond syntax highlighting
- Adding the
ngx-markdownclipboardplugin (can be added as a follow-up)
Further Notes
- The project uses Angular 22 with standalone components.
provideMarkdown()(function-based provider) is preferred overMarkdownModule.forRoot()for standalone apps. - The
markedversion should be managed as a peer dependency of ngx-markdown — do not pin a separate version. - ngx-markdown v21.x supports Angular 22. Verify compatibility before installing.
- The tab preprocessor is the only custom Markdown syntax. If marked's extension API cannot cleanly handle the
=== "Tab Name"tokenisation, fall back to pre-processing the string before passing to ngx-markdown's[data]input (similar to the current approach but outside the component).