feat: add PowerShellHighlighter syntax highlighter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-13 02:09:01 -04:00
parent 054f3acd67
commit a88b9c5483

View File

@@ -0,0 +1,97 @@
/**
* 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<string, string> = {
comment: 'text-[#8b949e]',
string: 'text-[#a5d6ff]',
placeholder: 'text-amber-400 underline decoration-dashed',
variable: 'text-[#79c0ff]',
cmdlet: 'text-[#22d3ee]',
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
}
export function PowerShellHighlighter({ script }: Props) {
const parts: React.ReactNode[] = []
let lastIndex = 0
TOKEN_REGEX.lastIndex = 0
let match: RegExpExecArray | null
while ((match = TOKEN_REGEX.exec(script)) !== null) {
if (match.index > lastIndex) {
parts.push(script.slice(lastIndex, match.index))
}
const token = match[0]
const kind = classify(token)
parts.push(
<span key={match.index} className={TOKEN_CLASSES[kind]}>
{token}
</span>
)
lastIndex = match.index + token.length
}
if (lastIndex < script.length) {
parts.push(script.slice(lastIndex))
}
return (
<pre className="font-label text-sm bg-card rounded-xl p-4 overflow-x-auto">
<code>{parts}</code>
</pre>
)
}