/** * Single-pass PowerShell syntax highlighter. * * Uses a combined alternation regex so tokens matched earlier in the list * cannot be re-coloured by later rules (e.g. a variable inside a string * is captured by the string rule and won't be re-matched by the variable rule). * * Priority order: * 1. Comments /#[^\r\n]star/ * 2. String literals /"[^"]*"|'[^']*'/ * 3. Unfilled placeholders /\{\{[^}]+\}\}/ * 4. Variables /\$\w+/ * 5. Cmdlets /[A-Z][a-z]+-[A-Z][a-zA-Z]+/ * 6. Parameters /-[A-Za-z]+/ * 7. Keywords /\b(if|else|...)\b/ * * Note: variables (priority 4) consume $foreach before keywords (priority 7) * can match — this is intentional PowerShell behaviour. */ import React from 'react' const TOKEN_REGEX = new RegExp( [ /#[^\r\n]*/, // 1. comments /"[^"]*"|'[^']*'/, // 2. string literals /\{\{[^}]+\}\}/, // 3. unfilled placeholders /\$\w+/, // 4. variables /[A-Z][a-z]+-[A-Z][a-zA-Z]+/, // 5. cmdlets (Verb-Noun) /-[A-Za-z]+/, // 6. parameters /\b(?:if|else|elseif|foreach|for|while|function|return|try|catch|finally|param|switch)\b/, // 7. keywords ] .map(r => r.source) .join('|'), 'g' ) const TOKEN_CLASSES: Record = { comment: 'text-[#8b949e]', string: 'text-[#a5d6ff]', placeholder: 'text-amber-400 underline decoration-dashed', variable: 'text-[#79c0ff]', cmdlet: 'text-primary', parameter: 'text-[#d2a8ff]', keyword: 'text-[#ff7b72]', } const KEYWORDS = new Set([ 'if', 'else', 'elseif', 'foreach', 'for', 'while', 'function', 'return', 'try', 'catch', 'finally', 'param', 'switch', ]) function classify(token: string): string { if (token.startsWith('#')) return 'comment' if (token.startsWith('"') || token.startsWith("'")) return 'string' if (token.startsWith('{{')) return 'placeholder' if (token.startsWith('$')) return 'variable' if (/^-[A-Za-z]+$/.test(token)) return 'parameter' if (KEYWORDS.has(token)) return 'keyword' return 'cmdlet' } interface Props { script: string className?: string } export function PowerShellHighlighter({ script, className }: Props) { const parts: React.ReactNode[] = [] let lastIndex = 0 const tokenRegex = new RegExp(TOKEN_REGEX.source, TOKEN_REGEX.flags) let match: RegExpExecArray | null while ((match = tokenRegex.exec(script)) !== null) { if (match.index > lastIndex) { parts.push(script.slice(lastIndex, match.index)) } const token = match[0] const kind = classify(token) parts.push( {token} ) lastIndex = match.index + token.length } if (lastIndex < script.length) { parts.push(script.slice(lastIndex)) } return (
      {parts}
    
) }