Skip to main content

Markdown Rendering with Syntax Highlighting Implementation

Overview

This document explains the implementation of markdown rendering with syntax highlighting for LLM (Large Language Model) responses in an Angular 17 chat application. The goal was to transform plain text LLM responses containing markdown into properly formatted, syntax-highlighted content.

Initial Problem

The chat application was displaying LLM messages as plain text using simple Angular interpolation ({{ message.content }}). This meant that markdown formatting like bold, italic, code blocks, lists, and other markdown elements were not being rendered properly - they appeared as raw markdown syntax.

Example of the Problem:

Input: "Here's some **bold text** and a code example:\n```typescript\nconst hello = 'world';\n```"
Output: "Here's some **bold text** and a code example:\n```typescript\nconst hello = 'world';\n```"

Solution Overview

We implemented a comprehensive solution using:

  1. ngx-markdown - Angular library for markdown rendering
  2. Prism.js - Syntax highlighting library
  3. Custom styling - Themed to match the chat interface
  4. Service architecture - Clean, reusable implementation

Implementation Steps

Step 1: Library Selection and Installation

Decision: Why ngx-markdown?

  • Specifically designed for Angular applications
  • Built-in security (XSS protection)
  • Easy integration with syntax highlighting
  • Support for custom renderers
  • Perfect for future code copying functionality

Installation:

npm install ngx-markdown@17.2.0 marked@9.1.6
npm install prismjs @types/prismjs

Version Compatibility Issue:

  • Initial attempt to install latest ngx-markdown (v20.0.0) failed
  • Problem: Version mismatch - ngx-markdown v20 requires Angular 20, but project uses Angular 17
  • Solution: Installed compatible version ngx-markdown@17.2.0

Step 2: Basic Markdown Integration

Updated main.ts:

import { MarkdownModule, MARKED_OPTIONS } from 'ngx-markdown';
import { HttpClientModule } from '@angular/common/http';

bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
// ... other modules
HttpClientModule, // Required for ngx-markdown
MarkdownModule.forRoot({
markedOptions: {
provide: MARKED_OPTIONS,
useValue: {
gfm: true, // GitHub Flavored Markdown
breaks: false, // Don't convert \n to <br>
pedantic: false, // Don't be strict about markdown
},
},
})
)
]
});

Updated MessageBubbleComponent:

// Added MarkdownModule to imports
imports: [CommonModule, MarkdownModule]

// Updated template to conditionally render markdown
<div class="message-content">
@if (message.isUser) {
{{ message.content }} <!-- Plain text for user messages -->
} @else {
<markdown [data]="message.content"></markdown> <!-- Markdown for AI messages -->
}
</div>

Step 3: Syntax Highlighting Implementation

Initial Approach - Direct Prism Import:

// This caused issues
import 'prismjs';
import 'prismjs/components/prism-typescript';
// ... more language imports

Problems Encountered:

  1. CommonJS Warning:

    Warning: CommonJS or AMD dependencies can cause optimization bailouts.
    For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies
  2. TypeScript Error:

    error TS2693: 'MarkedOptions' only refers to a type, but is being used as a value here.
  3. Type Declaration Errors:

    error TS7016: Could not find a declaration file for module 'prismjs/components/prism-typescript'

Solutions Applied:

Solution 1: CommonJS Configuration

Problem: Angular's build optimizer doesn't handle CommonJS modules well Solution: Added allowedCommonJsDependencies to angular.json

{
"build": {
"options": {
"allowedCommonJsDependencies": [
"prismjs"
]
}
}
}

Solution 2: Fixed MarkedOptions Usage

Problem: Trying to use TypeScript type as a runtime value Solution: Used proper injection token

// Wrong:
provide: MarkedOptions,
useFactory: markedOptionsFactory,

// Correct:
provide: MARKED_OPTIONS,
useValue: {
gfm: true,
breaks: false,
pedantic: false,
},

Solution 3: Custom Type Declarations

Problem: Missing TypeScript declarations for Prism language components Solution: Created custom type declarations

Created src/types/prism.d.ts:

declare module 'prismjs/components/prism-*.js' {
const content: any;
export default content;
}

declare module 'prismjs/components/prism-*' {
const content: any;
export default content;
}

Updated tsconfig.json:

{
"compilerOptions": {
"typeRoots": [
"node_modules/@types",
"src/types" // Added custom types
]
}
}

Step 4: Service Architecture

Created PrismService for Better Management:

@Injectable({
providedIn: 'root'
})
export class PrismService {
private prismLoaded = false;

async loadPrism(): Promise<any> {
if (this.prismLoaded) {
return Promise.resolve(typeof Prism !== 'undefined' ? Prism : null);
}

try {
await import('prismjs');
await this.loadLanguageComponents();
this.prismLoaded = true;
return typeof Prism !== 'undefined' ? Prism : null;
} catch (error) {
console.error('Failed to load Prism:', error);
return null;
}
}

private async loadLanguageComponents(): Promise<void> {
const languages = [
'typescript', 'javascript', 'css', 'scss', 'json',
'markdown', 'bash', 'python', 'java', 'csharp', 'sql'
];

// Sequential loading to avoid conflicts
for (const lang of languages) {
try {
await import(`prismjs/components/prism-${lang}.js`);
} catch (error) {
console.warn(`Failed to load Prism language: ${lang}`, error);
}
}
}

async highlightElement(element: HTMLElement): Promise<void> {
const prism = await this.loadPrism();
if (prism && prism.highlightElement) {
try {
prism.highlightElement(element);
} catch (error) {
console.warn('Failed to highlight element:', error);
}
}
}
}

Benefits of Service Architecture:

  • Centralized logic: All Prism-related code in one place
  • Reusable: Other components can use the service
  • Error handling: Graceful fallbacks
  • Performance: Lazy loading of languages
  • Maintainable: Easy to add new languages

Step 5: Component Integration

Updated MessageBubbleComponent:

export class MessageBubbleComponent implements AfterViewInit {
@Input() message!: Message;
@ViewChild('messageContent') messageContent!: ElementRef;

constructor(private prismService: PrismService) {}

ngAfterViewInit(): void {
this.highlightCode();
}

onMarkdownReady(): void {
// Highlight after markdown renders
setTimeout(() => this.highlightCode(), 0);
}

private async highlightCode(): Promise<void> {
if (this.messageContent && !this.message.isUser) {
const codeBlocks = this.messageContent.nativeElement.querySelectorAll('pre code');
for (const block of codeBlocks) {
await this.prismService.highlightElement(block as HTMLElement);
}
}
}
}

Step 6: Custom Styling

Added Comprehensive CSS in styles.scss:

/* Import Prism theme */
@import 'prismjs/themes/prism.css';

/* Custom Prism variables */
:root {
--prism-background: #f8f9fa;
--prism-text: #24292e;
--prism-comment: #6a737d;
--prism-keyword: #d73a49;
--prism-string: #032f62;
--prism-function: #6f42c1;
--prism-number: #005cc5;
--prism-operator: #d73a49;
}

/* Enhanced styling for code blocks */
pre[class*="language-"] {
padding: 1em !important;
margin: 0.8em 0 !important;
overflow: auto;
border-radius: 8px !important;
border: 1px solid rgba(0, 0, 0, 0.1) !important;
}

/* Dark theme for AI messages */
.ai-bubble {
--prism-background: rgba(255, 255, 255, 0.05);
--prism-text: #e1e4e8;
--prism-comment: #8b949e;
--prism-keyword: #ff7b72;
--prism-string: #a5d6ff;
--prism-function: #d2a8ff;
--prism-number: #79c0ff;
--prism-operator: #ff7b72;
}

Final Architecture

src/
├── main.ts # Bootstrap with MarkdownModule
├── styles.scss # Global Prism styling
├── types/
│ └── prism.d.ts # Custom type declarations
├── app/
│ ├── services/
│ │ └── prism.service.ts # Centralized Prism logic
│ └── components/
│ └── message-bubble/
│ └── message-bubble.component.ts # Updated with markdown rendering

Problems Solved

ProblemRoot CauseSolution
Version Compatibilityngx-markdown v20 requires Angular 20Used compatible version 17.2.0
CommonJS WarningsAngular build optimizer issuesAdded allowedCommonJsDependencies
TypeScript Type ErrorUsing type as valueUsed MARKED_OPTIONS token
Missing Type DeclarationsPrism components lack TypeScript definitionsCreated custom .d.ts files
Performance IssuesSynchronous imports blockingAsync service with lazy loading
Error HandlingNo fallbacks for failed importsTry-catch with graceful degradation

Features Achieved

✅ Markdown Rendering

  • Headers: H1-H6 with proper sizing
  • Text formatting: Bold, italic, strikethrough
  • Lists: Ordered and unordered
  • Links: Clickable with hover effects
  • Blockquotes: Styled with left border
  • Tables: Full table support with borders

✅ Syntax Highlighting

  • Languages: TypeScript, JavaScript, Python, CSS, JSON, SQL, Java, C#, Bash
  • Themes: Light theme for general use, dark theme for AI messages
  • Performance: Lazy loading, sequential language loading
  • Error handling: Graceful fallbacks

✅ User Experience

  • Conditional rendering: Plain text for users, markdown for AI
  • Responsive design: Works on mobile and desktop
  • Accessibility: Proper semantic HTML
  • Performance: No blocking operations

Future Enhancements

  1. Code Copying: Add copy buttons to code blocks
  2. More Languages: Go, Rust, PHP, Swift, Kotlin
  3. Custom Themes: User-selectable color schemes
  4. Line Numbers: Optional line numbering for code blocks
  5. Code Folding: Collapsible large code blocks

Lessons Learned

  1. Version Compatibility: Always check library compatibility with your Angular version
  2. Build Configuration: Modern Angular requires explicit CommonJS dependency declarations
  3. Type Safety: Custom type declarations are sometimes necessary for third-party libraries
  4. Service Architecture: Centralizing complex logic in services improves maintainability
  5. Error Handling: Always implement graceful fallbacks for external dependencies
  6. Performance: Lazy loading and async operations prevent blocking the UI

Testing

To test the implementation, send messages with markdown content:

Here's a **bold** statement with some `inline code`.

## Code Example

```typescript
interface User {
name: string;
age: number;
}

const user: User = {
name: "John Doe",
age: 30
};
```

### List Example
- Item 1
- Item 2
- Item 3

> This is a blockquote with important information.

The result should be properly formatted markdown with syntax-highlighted code blocks, demonstrating the successful implementation of the entire system.