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:

  1. 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.
  2. No copy-to-clipboard — developers cannot quickly copy code snippets from rendered specs.
  3. Unsanitised innerHTML — the component sets innerHTML directly without invoking DomSanitizer, creating a potential XSS vector if spec content is ever sourced from untrusted input.
  4. 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
  • MarkdownService for 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. As a developer, I want syntax highlighting themes to match the existing dark code block styling (#1e293b background), so that the visual design remains consistent.
  8. 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.
  9. As a developer, I want the option to enable Mermaid diagram rendering in the future, so that spec documents can include architecture diagrams inline.
  10. 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.
  11. 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.
  12. As a developer, I want the old custom MarkdownComponent file 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 marked import in the custom component (ngx-markdown wraps it)
  • Keep: marked in package.json as 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:

  1. Registers a custom tokeniser for lines matching === "Tab Name" followed by 4-space indented content
  2. Emits custom HTML blocks (<div class="md-tabs">...) during the render phase
  3. Is registered via MARKED_EXTENSIONS injection 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.json scripts/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 #1e293b background (e.g. prism-okaidia or prism-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.ts to be a thin wrapper around ngx-markdown's MarkdownDirective with the app-markdown selector 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 markedOptions extension

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-label styles (custom tab UI)
  • Remove: .markdown-body pre, .markdown-body code base styles that duplicate what Prism's theme provides
  • Keep: .markdown-body h1h4, p, ul, ol, li, blockquote, table styles (ngx-markdown outputs .markdown-body class by default)
  • Add: Prism theme CSS import (via angular.json or 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 build completes without errors and the app launches in Tauri without console errors
  • Optional: Add a single markdown.component.spec.ts that asserts MarkdownService.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-markdown clipboard plugin (can be added as a follow-up)

Further Notes

  • The project uses Angular 22 with standalone components. provideMarkdown() (function-based provider) is preferred over MarkdownModule.forRoot() for standalone apps.
  • The marked version 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).