typescript
68 lines · 10 steps
How a regex tokenizer highlights code
A small lexer walks source text with ordered regex rules, then wraps each token in colored HTML spans.
Explained by
highlit
1type TokenType = "keyword" | "string" | "comment" | "number" | "text";
2
3interface Token {
4 type: TokenType;
5 value: string;
6}
7
8const KEYWORDS = new Set([
9 "const", "let", "var", "function", "return", "if", "else",
10 "for", "while", "class", "import", "export", "new", "typeof",
11]);
12
13const RULES: Array<{ type: TokenType; pattern: RegExp }> = [
14 { type: "comment", pattern: /^\/\/[^\n]*|^\/\*[\s\S]*?\*\// },
15 { type: "string", pattern: /^"(?:\\.|[^"\\])*"|^'(?:\\.|[^'\\])*'|^`(?:\\.|[^`\\])*`/ },
16 { type: "number", pattern: /^\d+(?:\.\d+)?/ },
17 { type: "text", pattern: /^[A-Za-z_$][\w$]*/ },
18 { type: "text", pattern: /^\s+/ },
19 { type: "text", pattern: /^[^\w\s]/ },
20];
21
22export function tokenize(source: string): Token[] {
23 const tokens: Token[] = [];
24 let rest = source;
25
26 while (rest.length > 0) {
27 let matched = false;
28
29 for (const { type, pattern } of RULES) {
30 const match = pattern.exec(rest);
31 if (!match) continue;
32
33 const value = match[0];
34 const resolved: TokenType =
35 type === "text" && KEYWORDS.has(value) ? "keyword" : type;
36
37 tokens.push({ type: resolved, value });
38 rest = rest.slice(value.length);
39 matched = true;
40 break;
41 }
42
43 if (!matched) {
44 tokens.push({ type: "text", value: rest[0] });
45 rest = rest.slice(1);
46 }
47 }
48
49 return tokens;
50}
51
52const COLORS: Record<TokenType, string> = {
53 keyword: "#c678dd",
54 string: "#98c379",
55 comment: "#5c6370",
56 number: "#d19a66",
57 text: "#abb2bf",
58};
59
60export function highlight(source: string): string {
61 return tokenize(source)
62 .map(({ type, value }) => {
63 const escaped = value.replace(/&/g, "&").replace(/</g, "<");
64 if (type === "text") return escaped;
65 return `<span style="color:${COLORS[type]}">${escaped}</span>`;
66 })
67 .join("");
68}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Ordering rules so the most specific patterns match first is what keeps a greedy lexer correct.
- 2Anchoring every pattern to the start of the remaining input lets you consume the string one token at a time.
- 3Separating tokenizing from rendering keeps the lexer reusable for outputs other than HTML.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-regex-tokenizer-highlights-code-explained-typescript-c3b7/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.