# OverType Syntax Highlighting API
OverType provides a simple yet powerful API for integrating custom syntax highlighting libraries with your markdown editor. This document explains how to use the highlighting API and provides examples for popular highlighting libraries.
## Overview
The OverType syntax highlighting API allows you to:
- **Global Highlighting**: Set a highlighter that applies to all OverType instances
- **Per-Instance Highlighting**: Set a highlighter for specific editor instances
- **Library Agnostic**: Works with any highlighting library (Shiki, Prism, highlight.js, etc.)
- **Real-time**: Highlights code as you type
- **Preserves Alignment**: Maintains perfect character positioning for the WYSIWYG experience
## Basic Usage
### Global Code Highlighter
```javascript
// Set a global highlighter that applies to all OverType instances
OverType.setCodeHighlighter((code, language) => {
// Your highlighting logic here
return highlightedHtml;
});
```
### Per-Instance Code Highlighter
```javascript
// Option 1: Set during initialization
const [editor] = new OverType('#editor', {
codeHighlighter: (code, language) => {
return highlightedHtml;
}
});
// Option 2: Set after initialization
editor.setCodeHighlighter((code, language) => {
return highlightedHtml;
});
```
### Disable Highlighting
```javascript
// Disable global highlighting
OverType.setCodeHighlighter(null);
// Disable per-instance highlighting
editor.setCodeHighlighter(null);
```
## API Contract
### Highlighter Function Signature
```javascript
function highlighter(code, language) {
// Parameters:
// - code: string - The raw code content to highlight
// - language: string - Language extracted from fence (e.g., 'javascript', 'python', '')
// Returns:
// - string - HTML with syntax highlighting
}
```
### Requirements
1. **Preserve Character Positions**: The returned HTML must maintain the same character positions as the input
2. **Handle Unknown Languages**: Should gracefully handle languages not supported by your highlighter
3. **Escape HTML**: Must return properly escaped HTML if the highlighter doesn't handle escaping
4. **Performance**: Should be fast enough for real-time highlighting (consider debouncing for heavy highlighters)
5. **Error Handling**: Should not throw errors; fallback to plain text if highlighting fails
## Examples
### 1. Simple Pattern-Based Highlighter
```javascript
function simpleHighlighter(code, language) {
return code
// Keywords
.replace(/\b(function|const|let|var|if|else|for|while|return|class)\b/g,
'$1')
// Strings
.replace(/(["'])((?:\\.|(?!\1)[^\\])*?)\1/g,
'$1$2$1')
// Comments
.replace(/(\/\/.*$|#.*$)/gm,
'$1')
// Numbers
.replace(/\b(\d+(?:\.\d+)?)\b/g,
'$1');
}
OverType.setCodeHighlighter(simpleHighlighter);
```
### 2. Shiki.js Integration (v3.0+)
```javascript
import { codeToHtml } from 'shiki';
// Async highlighter function
async function shikiHighlighter(code, language) {
try {
// Map common aliases
const langMap = {
'js': 'javascript',
'ts': 'typescript',
'py': 'python',
'rs': 'rust'
};
const normalizedLang = langMap[language] || language || 'text';
const highlighted = await codeToHtml(code, {
lang: normalizedLang,
theme: 'github-light'
});
// Extract inner HTML from pre>code element
const match = highlighted.match(/]*>([\s\S]*?)<\/code>/);
return match ? match[1] : code;
} catch (error) {
console.warn('Shiki highlighting failed:', error);
return code; // Fallback to plain text
}
}
// Synchronous wrapper with caching for real-time highlighting
const highlightCache = new Map();
function syncShikiHighlighter(code, language) {
const cacheKey = `${language}:${code.substring(0, 100)}`;
if (highlightCache.has(cacheKey)) {
return highlightCache.get(cacheKey);
}
// Start async highlighting
shikiHighlighter(code, language).then(result => {
highlightCache.set(cacheKey, result);
// Trigger re-render
OverType.setCodeHighlighter(syncShikiHighlighter);
});
return code; // Return plain code while highlighting
}
OverType.setCodeHighlighter(syncShikiHighlighter);
```
### 2b. Shiki.js Legacy (v0.14)
```javascript
import { getHighlighter } from 'shiki@0.14.7';
let shikiHighlighter = null;
async function initShiki() {
shikiHighlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: ['javascript', 'typescript', 'python', 'rust', 'go']
});
OverType.setCodeHighlighter((code, language) => {
if (!shikiHighlighter) return code;
try {
const langMap = {
'js': 'javascript',
'ts': 'typescript',
'py': 'python',
'rs': 'rust'
};
const normalizedLang = langMap[language] || language || 'text';
if (!shikiHighlighter.getLoadedLanguages().includes(normalizedLang)) {
return code;
}
const highlighted = shikiHighlighter.codeToHtml(code, {
lang: normalizedLang,
theme: 'github-light'
});
const match = highlighted.match(/]*>([\s\S]*?)<\/code>/);
return match ? match[1] : code;
} catch (error) {
console.warn('Shiki highlighting failed:', error);
return code;
}
});
}
initShiki();
```
### 3. Prism.js Integration
```javascript
import Prism from 'prismjs';
// Import languages you need
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-rust';
function prismHighlighter(code, language) {
try {
// Map aliases
const langMap = {
'js': 'javascript',
'py': 'python',
'rs': 'rust'
};
const normalizedLang = langMap[language] || language;
if (Prism.languages[normalizedLang]) {
return Prism.highlight(code, Prism.languages[normalizedLang], normalizedLang);
}
return code; // Fallback for unsupported languages
} catch (error) {
console.warn('Prism highlighting failed:', error);
return code;
}
}
OverType.setCodeHighlighter(prismHighlighter);
```
### 4. highlight.js Integration
```javascript
import hljs from 'highlight.js';
function hljsHighlighter(code, language) {
try {
if (language && hljs.getLanguage(language)) {
const result = hljs.highlight(code, { language });
return result.value;
} else {
// Auto-detect language
const result = hljs.highlightAuto(code);
return result.value;
}
} catch (error) {
console.warn('highlight.js highlighting failed:', error);
return hljs.util.escapeHtml(code);
}
}
OverType.setCodeHighlighter(hljsHighlighter);
```
### 5. Language-Specific Highlighters
```javascript
// Different highlighters for different languages
function multiHighlighter(code, language) {
switch (language) {
case 'json':
return highlightJson(code);
case 'sql':
return highlightSql(code);
case 'javascript':
case 'js':
return highlightJavaScript(code);
default:
return simpleHighlighter(code, language);
}
}
function highlightJson(code) {
return code
.replace(/(["'])((?:\\.|(?!\1)[^\\])*?)(\1)(\s*:\s*)/g,
'$1$2$3$4')
.replace(/:\s*(["'])((?:\\.|(?!\1)[^\\])*?)\1/g,
': $1$2$1')
.replace(/:\s*(\d+(?:\.\d+)?)/g,
': $1')
.replace(/:\s*(true|false|null)/g,
': $1');
}
OverType.setCodeHighlighter(multiHighlighter);
```
## Performance Considerations
### Debouncing for Heavy Highlighters
```javascript
let highlightTimeout;
function debouncedHighlighter(code, language) {
return new Promise((resolve) => {
clearTimeout(highlightTimeout);
highlightTimeout = setTimeout(() => {
resolve(heavyHighlighter(code, language));
}, 150); // 150ms debounce
});
}
// For async highlighters, you might need a synchronous wrapper
let highlightCache = new Map();
function cachedAsyncHighlighter(code, language) {
const cacheKey = `${language}:${code}`;
if (highlightCache.has(cacheKey)) {
return highlightCache.get(cacheKey);
}
// Start async highlighting
heavyAsyncHighlighter(code, language).then(result => {
highlightCache.set(cacheKey, result);
// Trigger re-render if needed
OverType.setCodeHighlighter(cachedAsyncHighlighter);
});
// Return plain text while highlighting is in progress
return code;
}
```
### Language Detection
```javascript
function detectLanguage(code, suggestedLanguage) {
// Use suggested language if valid
if (suggestedLanguage && supportedLanguages.includes(suggestedLanguage)) {
return suggestedLanguage;
}
// Simple heuristics for common languages
if (/^\s*{[\s\S]*}\s*$/.test(code.trim())) {
return 'json';
}
if (/\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE)\b/i.test(code)) {
return 'sql';
}
if (/\b(function|const|let|var|=>)\b/.test(code)) {
return 'javascript';
}
if (/\b(def|import|from|class|if __name__)\b/.test(code)) {
return 'python';
}
return 'text';
}
function smartHighlighter(code, language) {
const detectedLanguage = detectLanguage(code, language);
return actualHighlighter(code, detectedLanguage);
}
```
## Best Practices
1. **Always provide fallbacks**: If highlighting fails, return the original code
2. **Handle edge cases**: Empty strings, very large code blocks, unsupported languages
3. **Consider performance**: Use caching, debouncing, or web workers for heavy highlighting
4. **Test thoroughly**: Test with various languages, edge cases, and large documents
5. **Provide user feedback**: Show loading states or errors when appropriate
## Troubleshooting
### Common Issues
1. **Characters not aligning**: Make sure your highlighter preserves all whitespace and character positions
2. **Performance problems**: Consider debouncing or caching for expensive highlighting operations
3. **Languages not working**: Check that your highlighter library supports the requested language
4. **HTML escaping issues**: Ensure proper HTML escaping to prevent XSS vulnerabilities
### Debug Mode
```javascript
function debugHighlighter(code, language) {
console.log('Highlighting:', { language, codeLength: code.length });
try {
const result = yourHighlighter(code, language);
console.log('Highlight success:', { resultLength: result.length });
return result;
} catch (error) {
console.error('Highlight failed:', error);
return code;
}
}
OverType.setCodeHighlighter(debugHighlighter);
```
## Integration Examples
Complete integration examples are available in the `examples/` directory:
- `examples/syntax-highlighting-api.html` - Basic API demonstration
- `examples/shiki-integration.html` - Full Shiki.js integration with themes and language support
These examples show real-world usage patterns and can serve as starting points for your own implementations.