Von 60% auf 93,3% Genauigkeit: ML-gestützte Syntax-Hervorhebung mit Shiki
Wenn deine Regex-Patterns denken, alles wär JavaScript
- Die Migrations-Challenge
- Der Start mit Pattern Matching (60% Genauigkeit)
- Der Durchbruch: VS Codes ML-Models
- Die Synchron vs. Asynchron Challenge
- Die Hybrid-Solution bauen
- Integration in die Build Pipeline
- Real-World Performance
- Lessons Learned
- Implementation Tips
- Der Code
- Was kommt als Nächstes?
- Fazit
Die Migrations-Challenge
Letzten Monat hab ich beschlossen, das Syntax Highlighting meines Blogs von Prism.js auf Shiki upzugraden. Warum? Weil Shiki die gleiche Engine wie VS Code nutzt und damit diese wunderschöne, vertraute Syntax-Hervorhebung liefert, die wir alle lieben. Aber da war ein Haken: Mein Blog hatte hunderte Code-Blöcke ohne Language Specifier.
function detectLanguage(code) {
// Das könnte JavaScript sein... oder Python... oder Ruby?
return 'javascript'; // Im Zweifel isses wahrscheinlich JS 🤷
}
Kommt dir bekannt vor? Wenn du schon mal zwischen Syntax-Highlightern migriert bist, kennst du den Schmerz. Die einfache Lösung wär gewesen, manuell Language Specifier zu jedem Code-Block hinzuzufügen. Aber wo bleibt da der Spaß?
Der Start mit Pattern Matching (60% Genauigkeit)
Mein erster Versuch war ein klassischer Pattern-basierter Detector. Du kennst das Spiel: Nach import
-Statements suchen, auf Semikolons checken, nach sprachspezifischen Keywords scannen. So sah das aus:
function detectLanguage(code) {
// Python?
if (code.includes('def ') || code.includes('import ')) {
return 'python';
}
// JavaScript?
if (code.includes('const ') || code.includes('function ')) {
return 'javascript';
}
// Wenn alles andere fehlschlägt...
return 'text';
}
Die Ergebnisse waren… naja, nicht so geil:
=== Pattern Detection Results ===
Total tests: 30
Passed: 18 (60.0%)
Failed: 12 (40.0%)
Failed Languages:
- Python (0/2) - Als JavaScript erkannt
- Ruby (0/1) - Als JavaScript erkannt
- Java (0/1) - Als JavaScript erkannt
- Go (0/1) - Als TypeScript erkannt
- Rust (0/1) - Als Python erkannt
Anscheinend dachte mein Detector, alles wär JavaScript. Ist wie diese Hammer-Nagel-Situation, nur für Programmiersprachen.
Der Durchbruch: VS Codes ML-Models
Dann hab ich Microsofts @vscode/vscode-languagedetection
Package entdeckt. Das ist nicht nur ein weiterer Pattern Matcher – es ist das gleiche ML-Model, das VS Code für Language Detection nutzt, trainiert auf Millionen von Code-Samples.
Die Transformation war krass:
=== ML Detection Results ===
Total tests: 30
Passed: 28 (93.3%)
Failed: 2 (6.7%)
Perfekte Erkennung (100%):
- Python, Ruby, Go, Rust, C/C++, Java
- Swift, Kotlin, Lua, R
- SQL, YAML, JSON, HTML/XML
- Bash, PowerShell, Dockerfile
Nur 2 Failures:
- React JSX → TypeScript (verständlich)
- SCSS → CSS (sehr ähnlich)
Die Synchron vs. Asynchron Challenge
Aber hier wurde’s interessant. Eleventys Markdown-Processing ist synchron, während ML Detection asynchron ist. Ist wie der Versuch, nen eckigen Pfosten in ein rundes Loch zu stecken.
// Was Eleventy erwartet
function processMarkdown(content) {
return processedContent; // Sync
}
// Was ML Detection liefert
async function detectLanguage(code) {
return await mlModel.detect(code); // Async
}
Die Lösung? Ein zweistufiger Ansatz:
- Development Mode: Verbessertes Pattern Detection (80-85% Genauigkeit) für instant Feedback
- Build Mode: Files mit ML Detection pre-processen, bevor Eleventy sie sieht
Die Hybrid-Solution bauen
Stage 1: Verbessertes Pattern Detection
Erstmal hab ich nen smarteren Pattern Detector gebaut, der Languages in ner bestimmten Reihenfolge checkt, um False Positives zu vermeiden:
function detectLanguage(code) {
const firstLine = code.split('\n')[0].trim();
// 1. Shebang detection (höchste Priorität)
if (firstLine.startsWith('#!')) {
if (firstLine.includes('python')) return 'python';
if (firstLine.includes('node')) return 'javascript';
if (firstLine.includes('bash')) return 'bash';
}
// 2. Python VOR JavaScript checken
if (code.match(/^def\s+\w+.*:/m) ||
code.includes('self.') ||
code.includes('__init__')) {
return 'python';
}
// 3. Ruby (auch vor JS)
if (code.match(/^class\s+\w+\s*</m) ||
code.includes('puts ') ||
code.includes('do |')) {
return 'ruby';
}
// ... mehr patterns
}
Das hat die Accuracy auf ~85% geboostet – gut genug für Development.
Stage 2: ML Pre-processing
Für Production Builds hab ich nen Pre-processor gebaut, der vor Eleventy läuft:
async function preprocessMarkdown(content) {
const codeBlockRegex = /^```(\w*)\n([\s\S]*?)^```/gm;
const blocksToProcess = [];
// Unmarked code blocks finden
let match;
while ((match = codeBlockRegex.exec(content)) !== null) {
if (!match[1]) { // Keine Language specified
blocksToProcess.push({
fullMatch: match[0],
code: match[2],
index: match.index
});
}
}
// Languages parallel detecten
const detectedLanguages = await Promise.all(
blocksToProcess.map(block =>
detectLanguage(block.code)
)
);
// Blocks replacen (rückwärts um Indices zu behalten)
let processedContent = content;
for (let i = blocksToProcess.length - 1; i >= 0; i--) {
const block = blocksToProcess[i];
const lang = detectedLanguages[i];
const newBlock = `\`\`\`${lang}\n${block.code}\`\`\``;
processedContent =
processedContent.substring(0, block.index) +
newBlock +
processedContent.substring(block.index + block.fullMatch.length);
}
return processedContent;
}
Integration in die Build Pipeline
Das Schöne an diesem Approach ist die nahtlose Integration:
{
"scripts": {
"dev": "eleventy --serve",
"build": "npm run ml-detect && eleventy",
"build:no-ml": "eleventy",
"ml-detect": "node scripts/apply-ml-detection.js"
}
}
Während der Entwicklung kriegst du instant Feedback mit Pattern Detection. Für Production Builds läuft ML Detection automatisch und modifiziert deine Markdown-Files in-place, bevor Eleventy sie processed.
Real-World Performance
So sieht das in der Praxis aus:
$ npm run build
🤖 ML Language Detection anwenden...
ML Model initialisieren...
Model ready
./src/de/posts verarbeiten...
✓ 2024-07-26-git-worktree.md (142ms)
✓ 2024-08-15-docker-optimization.md (98ms)
✓ 2024-09-01-rust-async-patterns.md (156ms)
27 Files in 3.2s verarbeitet
Durchschnitt: 118ms pro File
Lessons Learned
1. Pattern Matching hat seine Grenzen
Egal wie clever deine Regex ist, sie kann nicht mit ML-Models konkurrieren, die auf Millionen von Beispielen trainiert wurden. Mein Pattern Detector hat Python mit JavaScript verwechselt, weil beide import
-Statements nutzen. Das ML-Model versteht den Kontext.
2. Hybride Ansätze funktionieren
Du brauchst nicht überall die “perfekte” Lösung. Patterns in Development (wo Speed zählt) und ML in Production (wo Accuracy zählt) zu nutzen, gibt dir das Beste aus beiden Welten.
3. Pre-processing > Runtime Processing
Anstatt mit Eleventys synchroner Natur zu kämpfen, war es simpler und wartbarer, drumherum mit Pre-processing zu arbeiten.
4. Cache alles
ML Detection ist teuer. Results basierend auf Code-Snippets zu cachen hat die Detection-Zeit bei nachfolgenden Runs um ~70% reduziert.
Implementation Tips
Wenn du was Ähnliches baust, hier meine Empfehlungen:
- Fang mit dem einfachsten Approach an: Bring erst Pattern Detection zum Laufen
- Test mit echten Daten: Meine 30-Language Test Suite hat Issues gefunden, die ich mir nie vorgestellt hätte
- Mach es idempotent: ML Detection mehrmals laufen zu lassen sollte safe sein
- Bau Escape Hatches ein: Der
build:no-ml
Command hat mich beim Debugging gerettet - Gib Progress Feedback: ML Detection kann langsam sein – lass User wissen, dass was passiert
Der Code
Die komplette Implementation ist in meinem Blog-Repository verfügbar. Hier die Key Files:
- Pattern Detector:
src/_utils/markdown-language-detector-improved.js
- ML Preprocessor:
src/_utils/markdown-preprocessor-ml.js
- Build Script:
scripts/apply-ml-detection.js
- Shiki Config:
.eleventy.js
Was kommt als Nächstes?
Diese Implementation läuft jetzt seit nem Monat in Production, und die Ergebnisse sprechen für sich. Code-Blöcke werden richtig highlighted, der Build-Process läuft smooth, und ich musste seit Wochen keine Language manuell angeben.
Zukünftige Verbesserungen könnten sein:
- Streaming Processing für große Files
- WebAssembly Version für Client-side Detection
- Custom Training für domänenspezifische Languages
- Integration mit Git Hooks für automatic Detection beim Commit
Fazit
Manchmal ist die beste Lösung nicht die eleganteste – es ist die, die funktioniert. Durch die Kombination von Pattern Matching für Dev-Speed mit ML Detection für Production-Accuracy hab ich ne 93,3% Detection Rate erreicht und dabei meinen Development Workflow schnell und responsive gehalten.
Das nächste Mal, wenn du vor ner ähnlichen Challenge stehst, denk dran: Du musst dich nicht zwischen Speed und Accuracy entscheiden. Manchmal kannst du beides haben.
Hast du schon mal mit automatic Language Detection in deinen Projekten gedealt? Würd mich mega interessieren, von deinem Approach zu hören! Schreib mir auf Twitter oder check die komplette Implementation auf GitHub.
Weiterführendes: