Skip to main content
Alvin QuachFull Stack Developer
HomeProjectsExperienceBlog
HomeProjectsExperienceBlog
alvinquach

Full Stack Developer building systems that respect complexity.

Open to opportunities

AQ

Projects

  • All Projects
  • Hoparc Physical Therapy
  • OpportunIQ
  • Hoop Almanac
  • SculptQL

Knowledge

  • Blog
  • Experience
  • Interview Prep

Connect

  • Contact
  • LinkedIn
  • GitHub
  • X

Resources

  • Resume
© 2026All rights reserved.
Back to Blogs
Decision
Featured
Depth: ●●○○○

Building a Custom Syntax Highlighter for My Portfolio's Code Editor

Why I built a token-based syntax highlighter from scratch instead of using Monaco or CodeMirror. The tradeoffs of lightweight custom code vs heavy editor libraries for read-only display.

Published January 24, 20264 min readImportance: ★★★★☆
Share:
syntax-highlighter.ts
typescript
// Token-based highlighting prevents double-matching
export function highlightCode(code: string, language: Language): string {
  const tokens: Array<{ placeholder: string; html: string }> = [];
  let tokenIndex = 0;
  let result = code;

  const createToken = (match: string, className: string): string => {
    const placeholder = `__TOKEN_${tokenIndex++}__`;
    tokens.push({
      placeholder,
      html: `<span class="${className}">${escapeHtml(match)}</span>`,
    });
    return placeholder;
  };

  // Phase 1: Replace matches with placeholders
  // Comments first (highest priority - nothing inside should match)
  result = result.replace(patterns.comments, (m) => createToken(m, 'text-gray-500'));
  result = result.replace(patterns.strings, (m) => createToken(m, 'text-amber-300'));
  result = result.replace(patterns.keywords, (m) => createToken(m, 'text-cyan-400'));
  // ... more patterns

  // Phase 2: Replace placeholders with styled HTML
  for (const { placeholder, html } of tokens) {
    result = result.replace(placeholder, html);
  }

  return result;
}

My portfolio's hero section features an interactive code editor that displays my developer profile as actual TypeScript and Python code. Users can switch languages, toggle comments, and explore different "files." The question was: how do I render syntax-highlighted code that looks professional without shipping a 2MB editor library?

The Options I Considered

Monaco Editor

Monaco is VS Code's editor engine. It's battle-tested, looks exactly like what developers use daily, and has excellent language support. But it's heavy (~2-3MB) and designed for editing, not display. For a read-only hero section, it felt like shipping a tank to deliver a letter.

CodeMirror 6

CodeMirror 6 is lighter (~200KB with languages) and modular. I actually use it in SculptQL for SQL editing where I need autocomplete and real editing features. But for read-only display? Still more than I needed.

Prism / Highlight.js

These are purpose-built for syntax highlighting. Lighter than editors, but they still require loading language definitions and themes. Plus, I wanted complete control over styling to match my portfolio's design system.

The Decision: Build Custom

I built a custom token-based syntax highlighter. The total code is ~150 lines, zero dependencies, and gives me complete control. Here's why this made sense:

1. Limited scope. I only need to highlight TypeScript and Python. I'm not building a general-purpose highlighter—just one that handles my specific code patterns (interfaces, types, dataclasses, comments).

2. Read-only display. No editing, no cursor, no selection. This eliminates 90% of what editor libraries provide.

3. Design system integration. I wanted syntax colors that match my portfolio's color palette exactly—cyan for keywords, amber for strings, gray for comments. Custom code means custom colors with no theme conflicts.

4. Bundle size matters. The hero section loads immediately. Adding 200KB+ just for syntax colors would hurt the very first impression visitors get.

The Token-Based Approach

The naive approach—running regex replacements directly on code—breaks when patterns overlap. A keyword inside a comment gets double-highlighted, producing broken HTML.

My solution uses a two-phase token system: First, scan the code and replace matches with unique placeholders (__TOKEN_0__, __TOKEN_1__, etc.). Store a mapping from placeholder to the final styled HTML. Second, after all patterns are processed, replace placeholders with their HTML spans.

This prevents double-highlighting because once a match becomes a placeholder, subsequent regex patterns won't match it.

Features I Built

Language switching: Toggle between TypeScript and Python. Each has its own code generator and highlighting patterns.

Comments toggle: Show or hide educational comments. The stripComments() function removes comments while preserving code structure.

File tabs: Switch between Developer, Skills, and Career "files"—each showing different aspects of my profile as code.

VS Code aesthetics: Traffic light buttons, sidebar explorer, status bar, output panel. The UI mimics VS Code without using any VS Code code.

Tradeoffs I Accepted

Not perfect highlighting. My regex patterns handle common cases but won't catch every edge case. Template literals with expressions, complex decorators, or unusual syntax might not highlight perfectly. For my use case (controlled, generated code), this is fine.

Maintenance burden. If I add a new language or need more sophisticated parsing, I'm on my own. For a portfolio that shows two languages I control, this is acceptable.

No accessibility features. Editor libraries handle screen readers, keyboard navigation, and focus management. My display-only component doesn't need these—it's visual decoration for the code I generate.

When NOT to Build Custom

For SculptQL (my SQL IDE), I use CodeMirror 6. Why? Because I need real editing: cursor positioning, autocomplete, keyboard shortcuts, undo/redo. Building that from scratch would be foolish.

The rule: use libraries when you need their features. Build custom when the library's overhead exceeds its value. For read-only syntax highlighting of code I control, custom was the right call.

The Result

~150 lines of highlighting code, zero dependencies, instant load, perfect design system integration, and a hero section that looks like VS Code without weighing like VS Code. Sometimes the best library is no library.