/ src / components / RegexHighlighter.svelte
RegexHighlighter.svelte
  1  <script lang="ts">
  2  interface HighlightPattern {
  3  	pattern: string;
  4  	color: string;
  5  	flags?: string;
  6  }
  7  
  8  interface Props {
  9  	patterns: HighlightPattern[];
 10  	text: string;
 11  }
 12  
 13  let { patterns, text }: Props = $props();
 14  
 15  interface Segment {
 16  	text: string;
 17  	color: string | null;
 18  }
 19  
 20  interface Line {
 21  	segments: Segment[];
 22  }
 23  
 24  function highlightLine(line: string, patterns: HighlightPattern[]): Segment[] {
 25  	if (!patterns.length || !line) return [{ text: line, color: null }];
 26  
 27  	// Track ranges with their colors - later patterns override earlier ones
 28  	const ranges: Array<{ start: number; end: number; color: string }> = [];
 29  
 30  	for (const { pattern, color, flags } of patterns) {
 31  		const regex = new RegExp(pattern, flags || "g");
 32  
 33  		let match = regex.exec(line);
 34  		while (match !== null) {
 35  			if (match[0].length === 0) {
 36  				regex.lastIndex++;
 37  				match = regex.exec(line);
 38  				continue;
 39  			}
 40  
 41  			const newStart = match.index;
 42  			const newEnd = match.index + match[0].length;
 43  
 44  			for (let i = ranges.length - 1; i >= 0; i--) {
 45  				const r = ranges[i];
 46  				if (r && r.start < newEnd && r.end > newStart) {
 47  					ranges.splice(i, 1);
 48  				}
 49  			}
 50  
 51  			ranges.push({ start: newStart, end: newEnd, color });
 52  			match = regex.exec(line);
 53  		}
 54  	}
 55  
 56  	ranges.sort((a, b) => a.start - b.start);
 57  
 58  	const segments: Segment[] = [];
 59  	let lastEnd = 0;
 60  
 61  	for (const range of ranges) {
 62  		if (range.start > lastEnd) {
 63  			segments.push({
 64  				text: line.slice(lastEnd, range.start),
 65  				color: null,
 66  			});
 67  		}
 68  
 69  		segments.push({
 70  			text: line.slice(range.start, range.end),
 71  			color: range.color,
 72  		});
 73  		lastEnd = range.end;
 74  	}
 75  
 76  	if (lastEnd < line.length) {
 77  		segments.push({ text: line.slice(lastEnd), color: null });
 78  	}
 79  
 80  	return segments.length ? segments : [{ text: line, color: null }];
 81  }
 82  
 83  function getLines(text: string, patterns: HighlightPattern[]): Line[] {
 84  	const rawLines = text.split("\n");
 85  	return rawLines.map((line) => ({
 86  		segments: highlightLine(line, patterns),
 87  	}));
 88  }
 89  
 90  let lines = $derived(getLines(text, patterns));
 91  </script>
 92  
 93  <div class="p-3 border-[0.5px] border-[#e8e0d9] rounded-md bg-[#fffaf5] overflow-x-auto mb-6">
 94  	<code class="regex-highlighter before:content-[''] after:content-[''] text-[#443d66]! font-mono! text-sm">
 95  		{#each lines as line}
 96  			<p class="whitespace-nowrap m-0! p-0! leading-relaxed">
 97  				{#each line.segments as segment}
 98  					{#if segment.color}
 99  						<span style="color: {segment.color}">{segment.text}</span>
100  					{:else}
101  						{segment.text}
102  					{/if}
103  				{/each}
104  			</p>
105  		{/each}
106  	</code>
107  </div>
108  
109  <style>
110  	.regex-highlighter {
111  		font-family: inherit;
112  		background: transparent;
113  		padding: 0;
114  	}
115  </style>