Von 60% auf 93,3% Genauigkeit: ML-gestützte Syntax-Hervorhebung mit Shiki

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

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:

  1. Development Mode: Verbessertes Pattern Detection (80-85% Genauigkeit) für instant Feedback
  2. 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:

  1. Fang mit dem einfachsten Approach an: Bring erst Pattern Detection zum Laufen
  2. Test mit echten Daten: Meine 30-Language Test Suite hat Issues gefunden, die ich mir nie vorgestellt hätte
  3. Mach es idempotent: ML Detection mehrmals laufen zu lassen sollte safe sein
  4. Bau Escape Hatches ein: Der build:no-ml Command hat mich beim Debugging gerettet
  5. 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: