diff --git a/SYNTAX_HIGHLIGHTING.md b/SYNTAX_HIGHLIGHTING.md new file mode 100644 index 0000000..d242e1b --- /dev/null +++ b/SYNTAX_HIGHLIGHTING.md @@ -0,0 +1,408 @@ +# 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. diff --git a/examples/highlightjs-integration.html b/examples/highlightjs-integration.html new file mode 100644 index 0000000..0b1dfba --- /dev/null +++ b/examples/highlightjs-integration.html @@ -0,0 +1,2220 @@ + + + + + + OverType + highlight.js Integration + + + + + + + +

OverType + highlight.js Integration

+

Real-time syntax highlighting with highlight.js auto-detection and 180+ languages

+ +
+ +
+
+ + +
+ + + +
+ +
Initializing highlight.js syntax highlighting...
+ +
+

About highlight.js Integration

+

This example demonstrates OverType's integration with highlight.js, a popular syntax highlighting library:

+ +
+
+

🎨 Themes

+

Choose from dozens of color themes to match your design

+
+
+

🔍 Auto-Detection

+

Automatically detects language when not specified

+
+
+

📚 180+ Languages

+

Supports virtually every programming language

+
+
+

⚡ Performance

+

Fast highlighting with minimal overhead

+
+
+

🔧 Easy Setup

+

Simple CDN integration, no build process required

+
+
+

📱 Universal

+

Works in all modern browsers and environments

+
+
+ +

Implementation

+

The integration uses OverType's setCodeHighlighter API with highlight.js's programmatic API for real-time highlighting as you type.

+
+ + + + + + + + + + + + + + + + + + diff --git a/examples/shiki-integration.html b/examples/shiki-integration.html new file mode 100644 index 0000000..a20612d --- /dev/null +++ b/examples/shiki-integration.html @@ -0,0 +1,1637 @@ + + + + + + OverType + Shiki.js Integration + + + +

OverType + Shiki.js Integration

+

Real-time syntax highlighting powered by Shiki's TextMate grammar engine

+ +
+ +
+ + + +
+ +
Initializing Shiki syntax highlighter...
+ +
+

About This Integration

+

This example demonstrates how to integrate Shiki.js with OverType for professional-grade syntax highlighting:

+
    +
  • TextMate Grammars: Uses the same syntax highlighting as VS Code
  • +
  • Multiple Themes: Supports dozens of color themes
  • +
  • Language Support: 100+ programming languages
  • +
  • Real-time: Highlights code as you type
  • +
  • Preserves Alignment: Maintains perfect character positioning
  • +
+

Implementation: The integration uses OverType's setCodeHighlighter API to provide a custom highlighting function that calls Shiki's core tokenizer.

+
+ + + + diff --git a/src/link-tooltip.js b/src/link-tooltip.js index 44f66dd..4deb92b 100644 --- a/src/link-tooltip.js +++ b/src/link-tooltip.js @@ -28,8 +28,7 @@ export class LinkTooltip { // Create tooltip element this.createTooltip(); - // Listen for cursor position changes - this.editor.textarea.addEventListener('selectionchange', () => this.checkCursorPosition()); + // Note: selectionchange is handled at document level in global listeners this.editor.textarea.addEventListener('keyup', (e) => { if (e.key.includes('Arrow') || e.key === 'Home' || e.key === 'End') { this.checkCursorPosition(); diff --git a/src/overtype.d.ts b/src/overtype.d.ts index c733d7b..426e6f6 100644 --- a/src/overtype.d.ts +++ b/src/overtype.d.ts @@ -2,6 +2,8 @@ // Project: https://github.com/panphora/overtype // Definitions generated from JSDoc comments and implementation +/// + export interface Theme { name: string; colors: { diff --git a/src/overtype.js b/src/overtype.js index 1504daa..f6ff00a 100644 --- a/src/overtype.js +++ b/src/overtype.js @@ -30,7 +30,7 @@ class OverType { constructor(target, options = {}) { // Convert target to array of elements let elements; - + if (typeof target === 'string') { elements = document.querySelectorAll(target); if (elements.length === 0) { @@ -73,10 +73,10 @@ class OverType { */ _init(element, options = {}) { this.element = element; - + // Store the original theme option before merging this.instanceTheme = options.theme || null; - + this.options = this._mergeOptions(options); this.instanceId = ++OverType.instanceCount; this.initialized = false; @@ -98,7 +98,7 @@ class OverType { // Setup shortcuts manager this.shortcuts = new ShortcutsManager(this); - + // Setup link tooltip this.linkTooltip = new LinkTooltip(this); @@ -107,11 +107,8 @@ class OverType { const toolbarButtons = typeof this.options.toolbar === 'object' ? this.options.toolbar.buttons : null; this.toolbar = new Toolbar(this, toolbarButtons); this.toolbar.create(); - - // Update toolbar states on selection change - this.textarea.addEventListener('selectionchange', () => { - this.toolbar.updateButtonStates(); - }); + + // Note: selectionchange only works on document level, handled in global listeners this.textarea.addEventListener('input', () => { this.toolbar.updateButtonStates(); }); @@ -138,17 +135,17 @@ class OverType { /* System-first, guaranteed monospaced; avoids Android 'ui-monospace' pitfalls */ fontFamily: '"SF Mono", SFMono-Regular, Menlo, Monaco, "Cascadia Code", Consolas, "Roboto Mono", "Noto Sans Mono", "Droid Sans Mono", "Ubuntu Mono", "DejaVu Sans Mono", "Liberation Mono", "Courier New", Courier, monospace', padding: '16px', - + // Mobile styles mobile: { fontSize: '16px', // Prevent zoom on iOS padding: '12px', lineHeight: 1.5 }, - + // Native textarea properties textareaProps: {}, - + // Behavior autofocus: false, autoResize: false, // Auto-expand height with content @@ -156,22 +153,23 @@ class OverType { maxHeight: null, // Maximum height for autoResize mode (null = unlimited) placeholder: 'Start typing...', value: '', - + // Callbacks onChange: null, onKeydown: null, - + // Features showActiveLineRaw: false, showStats: false, toolbar: false, statsFormatter: null, - smartLists: true // Enable smart list continuation + smartLists: true, // Enable smart list continuation + codeHighlighter: null // Per-instance code highlighter }; - + // Remove theme and colors from options - these are now global const { theme, colors, ...cleanOptions } = options; - + return { ...defaults, ...cleanOptions @@ -199,7 +197,7 @@ class OverType { if (themeName) { this.container.setAttribute('data-theme', themeName); } - + // If using instance theme, apply CSS variables to container if (this.instanceTheme) { const themeObj = typeof this.instanceTheme === 'string' ? getTheme(this.instanceTheme) : this.instanceTheme; @@ -211,7 +209,7 @@ class OverType { wrapper.parentNode.insertBefore(this.container, wrapper); this.container.appendChild(wrapper); } - + if (!this.wrapper) { // No valid structure found if (container) container.remove(); @@ -219,7 +217,7 @@ class OverType { this._buildFromScratch(); return; } - + this.textarea = this.wrapper.querySelector('.overtype-input'); this.preview = this.wrapper.querySelector('.overtype-preview'); @@ -232,7 +230,7 @@ class OverType { // Store reference on wrapper this.wrapper._instance = this; - + // Apply instance-specific styles via CSS custom properties if (this.options.fontSize) { this.wrapper.style.setProperty('--instance-font-size', this.options.fontSize); @@ -295,14 +293,14 @@ class OverType { // Create container that will hold toolbar and editor this.container = document.createElement('div'); this.container.className = 'overtype-container'; - + // Set theme on container - use instance theme if provided const themeToUse = this.instanceTheme || OverType.currentTheme || solar; const themeName = typeof themeToUse === 'string' ? themeToUse : themeToUse.name; if (themeName) { this.container.setAttribute('data-theme', themeName); } - + // If using instance theme, apply CSS variables to container if (this.instanceTheme) { const themeObj = typeof this.instanceTheme === 'string' ? getTheme(this.instanceTheme) : this.instanceTheme; @@ -311,12 +309,12 @@ class OverType { this.container.style.cssText += cssVars; } } - + // Create wrapper for editor this.wrapper = document.createElement('div'); this.wrapper.className = 'overtype-wrapper'; - - + + // Apply instance-specific styles via CSS custom properties if (this.options.fontSize) { this.wrapper.style.setProperty('--instance-font-size', this.options.fontSize); @@ -327,7 +325,7 @@ class OverType { if (this.options.padding) { this.wrapper.style.setProperty('--instance-padding', this.options.padding); } - + this.wrapper._instance = this; // Create textarea @@ -335,7 +333,7 @@ class OverType { this.textarea.className = 'overtype-input'; this.textarea.placeholder = this.options.placeholder; this._configureTextarea(); - + // Apply any native textarea properties if (this.options.textareaProps) { Object.entries(this.options.textareaProps).forEach(([key, value]) => { @@ -357,12 +355,12 @@ class OverType { // Assemble DOM this.wrapper.appendChild(this.textarea); this.wrapper.appendChild(this.preview); - + // No need to prevent link clicks - pointer-events handles this - + // Add wrapper to container first this.container.appendChild(this.wrapper); - + // Add stats bar at the end (bottom) if enabled if (this.options.showStats) { this.statsBar = document.createElement('div'); @@ -370,10 +368,10 @@ class OverType { this.container.appendChild(this.statsBar); this._updateStats(); } - + // Add container to element this.element.appendChild(this.container); - + // Debug logging if (window.location.pathname.includes('demo.html')) { console.log('_createDOM completed:', { @@ -384,14 +382,14 @@ class OverType { hasToolbar: this.options.toolbar }); } - + // Setup auto-resize if enabled if (this.options.autoResize) { this._setupAutoResize(); } else { // Ensure auto-resize class is removed if not using auto-resize this.container.classList.remove('overtype-auto-resize'); - + if (window.location.pathname.includes('demo.html')) { console.log('Removed auto-resize class from:', this.element.id); } @@ -421,7 +419,7 @@ class OverType { if (this.options.autofocus) { this.textarea.focus(); } - + // Setup or remove auto-resize if (this.options.autoResize) { if (!this.container.classList.contains('overtype-auto-resize')) { @@ -443,21 +441,21 @@ class OverType { const text = this.textarea.value; const cursorPos = this.textarea.selectionStart; const activeLine = this._getCurrentLine(text, cursorPos); - + // Parse markdown - const html = MarkdownParser.parse(text, activeLine, this.options.showActiveLineRaw); + const html = MarkdownParser.parse(text, activeLine, this.options.showActiveLineRaw, this.options.codeHighlighter); this.preview.innerHTML = html || 'Start typing...'; - + // Apply code block backgrounds this._applyCodeBlockBackgrounds(); - + // Links always have real hrefs now - no need to update them - + // Update stats if enabled if (this.options.showStats && this.statsBar) { this._updateStats(); } - + // Trigger onChange callback if (this.options.onChange && this.initialized) { this.options.onChange(text, this); @@ -471,26 +469,26 @@ class OverType { _applyCodeBlockBackgrounds() { // Find all code fence elements const codeFences = this.preview.querySelectorAll('.code-fence'); - + // Process pairs of code fences for (let i = 0; i < codeFences.length - 1; i += 2) { const openFence = codeFences[i]; const closeFence = codeFences[i + 1]; - + // Get parent divs const openParent = openFence.parentElement; const closeParent = closeFence.parentElement; - + if (!openParent || !closeParent) continue; - + // Make fences display: block openFence.style.display = 'block'; closeFence.style.display = 'block'; - + // Apply class to parent divs openParent.classList.add('code-block-line'); closeParent.classList.add('code-block-line'); - + // With the new structure, there's a
 block between fences, not DIVs
         // We don't need to process anything between the fences anymore
         // The 
 structure already handles the content correctly
@@ -522,21 +520,21 @@ class OverType {
       // Handle Tab key to prevent focus loss and insert spaces
       if (event.key === 'Tab') {
         event.preventDefault();
-        
+
         const start = this.textarea.selectionStart;
         const end = this.textarea.selectionEnd;
         const value = this.textarea.value;
-        
+
         // If there's a selection, indent/outdent based on shift key
         if (start !== end && event.shiftKey) {
           // Outdent: remove 2 spaces from start of each selected line
           const before = value.substring(0, start);
           const selection = value.substring(start, end);
           const after = value.substring(end);
-          
+
           const lines = selection.split('\n');
           const outdented = lines.map(line => line.replace(/^  /, '')).join('\n');
-          
+
           // Try to use execCommand first to preserve undo history
           if (document.execCommand) {
             // Select the text that needs to be replaced
@@ -553,10 +551,10 @@ class OverType {
           const before = value.substring(0, start);
           const selection = value.substring(start, end);
           const after = value.substring(end);
-          
+
           const lines = selection.split('\n');
           const indented = lines.map(line => '  ' + line).join('\n');
-          
+
           // Try to use execCommand first to preserve undo history
           if (document.execCommand) {
             // Select the text that needs to be replaced
@@ -579,12 +577,12 @@ class OverType {
             this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
           }
         }
-        
+
         // Trigger input event to update preview
         this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
         return;
       }
-      
+
       // Handle Enter key for smart list continuation
       if (event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
         if (this.handleSmartListContinuation()) {
@@ -592,10 +590,10 @@ class OverType {
           return;
         }
       }
-      
+
       // Let shortcuts manager handle other keys
       const handled = this.shortcuts.handleKeydown(event);
-      
+
       // Call user callback if provided
       if (!handled && this.options.onKeydown) {
         this.options.onKeydown(event, this);
@@ -610,15 +608,15 @@ class OverType {
       const textarea = this.textarea;
       const cursorPos = textarea.selectionStart;
       const context = MarkdownParser.getListContext(textarea.value, cursorPos);
-      
+
       if (!context || !context.inList) return false;
-      
+
       // Handle empty list item (exit list)
       if (context.content.trim() === '' && cursorPos >= context.markerEndPos) {
         this.deleteListMarker(context);
         return true;
       }
-      
+
       // Handle text splitting if cursor is in middle of content
       if (cursorPos > context.markerEndPos && cursorPos < context.lineEnd) {
         this.splitListItem(context, cursorPos);
@@ -626,15 +624,15 @@ class OverType {
         // Just add new item after current line
         this.insertNewListItem(context);
       }
-      
+
       // Handle numbered list renumbering
       if (context.listType === 'numbered') {
         this.scheduleNumberedListUpdate();
       }
-      
+
       return true;
     }
-    
+
     /**
      * Delete list marker and exit list
      * @private
@@ -643,11 +641,11 @@ class OverType {
       // Select from line start to marker end
       this.textarea.setSelectionRange(context.lineStart, context.markerEndPos);
       document.execCommand('delete');
-      
+
       // Trigger input event
       this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
     }
-    
+
     /**
      * Insert new list item
      * @private
@@ -655,11 +653,11 @@ class OverType {
     insertNewListItem(context) {
       const newItem = MarkdownParser.createNewListItem(context);
       document.execCommand('insertText', false, '\n' + newItem);
-      
+
       // Trigger input event
       this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
     }
-    
+
     /**
      * Split list item at cursor position
      * @private
@@ -667,23 +665,23 @@ class OverType {
     splitListItem(context, cursorPos) {
       // Get text after cursor
       const textAfterCursor = context.content.substring(cursorPos - context.markerEndPos);
-      
+
       // Delete text after cursor
       this.textarea.setSelectionRange(cursorPos, context.lineEnd);
       document.execCommand('delete');
-      
+
       // Insert new list item with remaining text
       const newItem = MarkdownParser.createNewListItem(context);
       document.execCommand('insertText', false, '\n' + newItem + textAfterCursor);
-      
+
       // Position cursor after new list marker
       const newCursorPos = this.textarea.selectionStart - textAfterCursor.length;
       this.textarea.setSelectionRange(newCursorPos, newCursorPos);
-      
+
       // Trigger input event
       this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
     }
-    
+
     /**
      * Schedule numbered list renumbering
      * @private
@@ -693,13 +691,13 @@ class OverType {
       if (this.numberUpdateTimeout) {
         clearTimeout(this.numberUpdateTimeout);
       }
-      
+
       // Schedule update after current input cycle
       this.numberUpdateTimeout = setTimeout(() => {
         this.updateNumberedLists();
       }, 10);
     }
-    
+
     /**
      * Update/renumber all numbered lists
      * @private
@@ -707,16 +705,16 @@ class OverType {
     updateNumberedLists() {
       const value = this.textarea.value;
       const cursorPos = this.textarea.selectionStart;
-      
+
       const newValue = MarkdownParser.renumberLists(value);
-      
+
       if (newValue !== value) {
         // Calculate cursor offset
         let offset = 0;
         const oldLines = value.split('\n');
         const newLines = newValue.split('\n');
         let charCount = 0;
-        
+
         for (let i = 0; i < oldLines.length && charCount < cursorPos; i++) {
           if (oldLines[i] !== newLines[i]) {
             const diff = newLines[i].length - oldLines[i].length;
@@ -726,12 +724,12 @@ class OverType {
           }
           charCount += oldLines[i].length + 1; // +1 for newline
         }
-        
+
         // Update textarea
         this.textarea.value = newValue;
         const newCursorPos = cursorPos + offset;
         this.textarea.setSelectionRange(newCursorPos, newCursorPos);
-        
+
         // Trigger update
         this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
       }
@@ -762,7 +760,7 @@ class OverType {
     setValue(value) {
       this.textarea.value = value;
       this.updatePreview();
-      
+
       // Update height if auto-resize is enabled
       if (this.options.autoResize) {
         this._updateAutoHeight();
@@ -778,7 +776,7 @@ class OverType {
      */
     getRenderedHTML(options = {}) {
       const markdown = this.getValue();
-      let html = MarkdownParser.parse(markdown);
+      let html = MarkdownParser.parse(markdown, -1, false, this.options.codeHighlighter);
       
       if (options.cleanHTML) {
         // Remove all syntax marker spans for clean HTML export
@@ -788,7 +786,7 @@ class OverType {
         // Clean up empty class attributes
         html = html.replace(/\sclass=""/g, '');
       }
-      
+
       return html;
     }
 
@@ -842,25 +840,34 @@ class OverType {
       this.updatePreview();
     }
 
+    /**
+     * Set instance-specific code highlighter
+     * @param {Function|null} highlighter - Function that takes (code, language) and returns highlighted HTML
+     */
+    setCodeHighlighter(highlighter) {
+      this.options.codeHighlighter = highlighter;
+      this.updatePreview();
+    }
+
     /**
      * Update stats bar
      * @private
      */
     _updateStats() {
       if (!this.statsBar) return;
-      
+
       const value = this.textarea.value;
       const lines = value.split('\n');
       const chars = value.length;
       const words = value.split(/\s+/).filter(w => w.length > 0).length;
-      
+
       // Calculate line and column
       const selectionStart = this.textarea.selectionStart;
       const beforeCursor = value.substring(0, selectionStart);
       const linesBeforeCursor = beforeCursor.split('\n');
       const currentLine = linesBeforeCursor.length;
       const currentColumn = linesBeforeCursor[linesBeforeCursor.length - 1].length + 1;
-      
+
       // Use custom formatter if provided
       if (this.options.statsFormatter) {
         this.statsBar.innerHTML = this.options.statsFormatter({
@@ -881,7 +888,7 @@ class OverType {
         `;
       }
     }
-    
+
     /**
      * Setup auto-resize functionality
      * @private
@@ -889,51 +896,51 @@ class OverType {
     _setupAutoResize() {
       // Add auto-resize class for styling
       this.container.classList.add('overtype-auto-resize');
-      
+
       // Store previous height for comparison
       this.previousHeight = null;
-      
+
       // Initial height update
       this._updateAutoHeight();
-      
+
       // Listen for input events
       this.textarea.addEventListener('input', () => this._updateAutoHeight());
-      
+
       // Listen for window resize
       window.addEventListener('resize', () => this._updateAutoHeight());
     }
-    
+
     /**
      * Update height based on scrollHeight
      * @private
      */
     _updateAutoHeight() {
       if (!this.options.autoResize) return;
-      
+
       const textarea = this.textarea;
       const preview = this.preview;
       const wrapper = this.wrapper;
-      
+
       // Get computed styles
       const computed = window.getComputedStyle(textarea);
       const paddingTop = parseFloat(computed.paddingTop);
       const paddingBottom = parseFloat(computed.paddingBottom);
-      
+
       // Store scroll positions
       const scrollTop = textarea.scrollTop;
-      
+
       // Reset height to get accurate scrollHeight
       textarea.style.setProperty('height', 'auto', 'important');
-      
+
       // Calculate new height based on scrollHeight
       let newHeight = textarea.scrollHeight;
-      
+
       // Apply min height constraint
       if (this.options.minHeight) {
         const minHeight = parseInt(this.options.minHeight);
         newHeight = Math.max(newHeight, minHeight);
       }
-      
+
       // Apply max height constraint
       let overflow = 'hidden';
       if (this.options.maxHeight) {
@@ -943,35 +950,35 @@ class OverType {
           overflow = 'auto';
         }
       }
-      
+
       // Apply the new height to all elements with !important to override base styles
       const heightPx = newHeight + 'px';
       textarea.style.setProperty('height', heightPx, 'important');
       textarea.style.setProperty('overflow-y', overflow, 'important');
-      
+
       preview.style.setProperty('height', heightPx, 'important');
       preview.style.setProperty('overflow-y', overflow, 'important');
-      
+
       wrapper.style.setProperty('height', heightPx, 'important');
-      
+
       // Restore scroll position
       textarea.scrollTop = scrollTop;
       preview.scrollTop = scrollTop;
-      
+
       // Track if height changed
       if (this.previousHeight !== newHeight) {
         this.previousHeight = newHeight;
         // Could dispatch a custom event here if needed
       }
     }
-    
+
     /**
      * Show or hide stats bar
      * @param {boolean} show - Whether to show stats
      */
     showStats(show) {
       this.options.showStats = show;
-      
+
       if (show && !this.statsBar) {
         // Create stats bar (add to container, not wrapper)
         this.statsBar = document.createElement('div');
@@ -984,7 +991,7 @@ class OverType {
         this.statsBar = null;
       }
     }
-    
+
     /**
      * Show or hide the plain textarea (toggle overlay visibility)
      * @param {boolean} show - true to show plain textarea (hide overlay), false to show overlay
@@ -998,7 +1005,7 @@ class OverType {
         // Show overlay mode (hide plain textarea text)
         this.container.classList.remove('plain-mode');
       }
-      
+
       // Update toolbar button if exists
       if (this.toolbar) {
         const toggleBtn = this.container.querySelector('[data-action="toggle-plain"]');
@@ -1008,7 +1015,7 @@ class OverType {
           toggleBtn.title = show ? 'Show markdown preview' : 'Show plain textarea';
         }
       }
-      
+
       return show;
     }
 
@@ -1025,7 +1032,7 @@ class OverType {
         // Show edit mode
         this.container.classList.remove('preview-mode');
       }
-      
+
       return show;
     }
 
@@ -1046,7 +1053,7 @@ class OverType {
       if (this.wrapper) {
         const content = this.getValue();
         this.wrapper.remove();
-        
+
         // Restore original content
         this.element.textContent = content;
       }
@@ -1111,7 +1118,23 @@ class OverType {
 
       OverType.stylesInjected = true;
     }
-    
+
+    /**
+     * Set global code highlighter for all OverType instances
+     * @param {Function|null} highlighter - Function that takes (code, language) and returns highlighted HTML
+     */
+    static setCodeHighlighter(highlighter) {
+      MarkdownParser.setCodeHighlighter(highlighter);
+
+      // Update all existing instances
+      document.querySelectorAll('.overtype-wrapper').forEach(wrapper => {
+        const instance = wrapper._instance;
+        if (instance && instance.updatePreview) {
+          instance.updatePreview();
+        }
+      });
+    }
+
     /**
      * Set global theme for all OverType instances
      * @param {string|Object} theme - Theme name or custom theme object
@@ -1120,18 +1143,18 @@ class OverType {
     static setTheme(theme, customColors = null) {
       // Process theme
       let themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
-      
+
       // Apply custom colors if provided
       if (customColors) {
         themeObj = mergeTheme(themeObj, customColors);
       }
-      
+
       // Store as current theme
       OverType.currentTheme = themeObj;
-      
+
       // Re-inject styles with new theme
       OverType.injectStyles(true);
-      
+
       // Update all existing instances - update container theme attribute
       document.querySelectorAll('.overtype-container').forEach(container => {
         const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
@@ -1139,7 +1162,7 @@ class OverType {
           container.setAttribute('data-theme', themeName);
         }
       });
-      
+
       // Also handle any old-style wrappers without containers
       document.querySelectorAll('.overtype-wrapper').forEach(wrapper => {
         if (!wrapper.closest('.overtype-container')) {
@@ -1148,7 +1171,7 @@ class OverType {
             wrapper.setAttribute('data-theme', themeName);
           }
         }
-        
+
         // Trigger preview update for the instance
         const instance = wrapper._instance;
         if (instance) {
@@ -1201,11 +1224,22 @@ class OverType {
             if (instance.options.showStats && instance.statsBar) {
               instance._updateStats();
             }
-            // Debounce updates
-            clearTimeout(instance._selectionTimeout);
-            instance._selectionTimeout = setTimeout(() => {
-              instance.updatePreview();
-            }, 50);
+            // Update toolbar button states if toolbar exists
+            if (instance.toolbar && instance.toolbar.updateButtonStates) {
+              instance.toolbar.updateButtonStates();
+            }
+            // Update link tooltip position if it exists
+            if (instance.linkTooltip && instance.linkTooltip.checkCursorPosition) {
+              instance.linkTooltip.checkCursorPosition();
+            }
+            // Only update preview if showing active line raw (which depends on cursor position)
+            if (instance.options.showActiveLineRaw) {
+              // Debounce updates
+              clearTimeout(instance._selectionTimeout);
+              instance._selectionTimeout = setTimeout(() => {
+                instance.updatePreview();
+              }, 50);
+            }
           }
         }
       });
@@ -1227,4 +1261,4 @@ OverType.currentTheme = solar;
 
 // Export for module systems
 export default OverType;
-export { OverType };
\ No newline at end of file
+export { OverType };
diff --git a/src/parser.js b/src/parser.js
index 52d5ca7..67549ee 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -10,6 +10,9 @@ export class MarkdownParser {
   // Track link index for anchor naming
   static linkIndex = 0;
 
+  // Global code highlighter function
+  static codeHighlighter = null;
+
   /**
    * Reset link index (call before parsing a new document)
    */
@@ -17,6 +20,13 @@ export class MarkdownParser {
     this.linkIndex = 0;
   }
 
+  /**
+   * Set global code highlighter function
+   * @param {Function|null} highlighter - Function that takes (code, language) and returns highlighted HTML
+   */
+  static setCodeHighlighter(highlighter) {
+    this.codeHighlighter = highlighter;
+  }
   /**
    * Escape HTML special characters
    * @param {string} text - Raw text to escape
@@ -427,9 +437,10 @@ export class MarkdownParser {
    * @param {string} text - Full markdown text
    * @param {number} activeLine - Currently active line index (optional)
    * @param {boolean} showActiveLineRaw - Show raw markdown on active line
+   * @param {Function} instanceHighlighter - Instance-specific code highlighter (optional)
    * @returns {string} Parsed HTML
    */
-  static parse(text, activeLine = -1, showActiveLineRaw = false) {
+  static parse(text, activeLine = -1, showActiveLineRaw = false, instanceHighlighter = null) {
     // Reset link counter for each parse
     this.resetLinkIndex();
 
@@ -466,19 +477,20 @@ export class MarkdownParser {
     const html = parsedLines.join('');
 
     // Apply post-processing for list consolidation
-    return this.postProcessHTML(html);
+    return this.postProcessHTML(html, instanceHighlighter);
   }
 
   /**
    * Post-process HTML to consolidate lists and code blocks
    * @param {string} html - HTML to post-process
+   * @param {Function} instanceHighlighter - Instance-specific code highlighter (optional)
    * @returns {string} Post-processed HTML with consolidated lists and code blocks
    */
-  static postProcessHTML(html) {
+  static postProcessHTML(html, instanceHighlighter = null) {
     // Check if we're in a browser environment
     if (typeof document === 'undefined' || !document) {
       // In Node.js environment - do manual post-processing
-      return this.postProcessHTMLManual(html);
+      return this.postProcessHTMLManual(html, instanceHighlighter);
     }
 
     // Parse HTML string into DOM
@@ -525,9 +537,25 @@ export class MarkdownParser {
 
             // Store reference to the code element for adding content
             currentCodeBlock._codeElement = codeElement;
+            currentCodeBlock._language = lang;
+            currentCodeBlock._codeContent = '';
             continue;
           } else {
-            // End of code block - fence stays visible
+            // End of code block - apply highlighting if needed
+            const highlighter = instanceHighlighter || this.codeHighlighter;
+            if (currentCodeBlock && highlighter && currentCodeBlock._codeContent) {
+              try {
+                const highlightedCode = highlighter(
+                  currentCodeBlock._codeContent,
+                  currentCodeBlock._language || ''
+                );
+                currentCodeBlock._codeElement.innerHTML = highlightedCode;
+              } catch (error) {
+                console.warn('Code highlighting failed:', error);
+                // Keep the plain text content as fallback
+              }
+            }
+
             inCodeBlock = false;
             currentCodeBlock = null;
             continue;
@@ -538,14 +566,18 @@ export class MarkdownParser {
       // Check if we're in a code block - any div that's not a code fence
       if (inCodeBlock && currentCodeBlock && child.tagName === 'DIV' && !child.querySelector('.code-fence')) {
         const codeElement = currentCodeBlock._codeElement || currentCodeBlock.querySelector('code');
-        // Add the line content to the code block
-        if (codeElement.textContent.length > 0) {
-          codeElement.textContent += '\n';
+        // Add the line content to the code block content (for highlighting)
+        if (currentCodeBlock._codeContent.length > 0) {
+          currentCodeBlock._codeContent += '\n';
         }
         // Get the actual text content, preserving spaces
-        // Use textContent instead of innerHTML to avoid double-escaping
-        // textContent automatically decodes HTML entities
         const lineText = child.textContent.replace(/\u00A0/g, ' '); // \u00A0 is nbsp
+        currentCodeBlock._codeContent += lineText;
+
+        // Also add to the code element (fallback if no highlighter)
+        if (codeElement.textContent.length > 0) {
+          codeElement.textContent += '\n';
+        }
         codeElement.textContent += lineText;
         child.remove();
         continue;
@@ -611,9 +643,10 @@ export class MarkdownParser {
   /**
    * Manual post-processing for Node.js environments (without DOM)
    * @param {string} html - HTML to post-process
+   * @param {Function} instanceHighlighter - Instance-specific code highlighter (optional)
    * @returns {string} Post-processed HTML
    */
-  static postProcessHTMLManual(html) {
+  static postProcessHTMLManual(html, instanceHighlighter = null) {
     let processed = html;
 
     // Process unordered lists
@@ -678,10 +711,22 @@ export class MarkdownParser {
       const lang = openFence.slice(3).trim();
       const langClass = lang ? ` class="language-${lang}"` : '';
 
+      // Apply code highlighting if available
+      let highlightedContent = codeContent;
+      const highlighter = instanceHighlighter || this.codeHighlighter;
+      if (highlighter) {
+        try {
+          highlightedContent = highlighter(codeContent, lang);
+        } catch (error) {
+          console.warn('Code highlighting failed:', error);
+          // Fall back to original content
+        }
+      }
+
       // Keep fence markers visible as separate divs, with pre/code block between them
       let result = `
${openFence}
`; - // Content is already escaped, don't double-escape - result += `
${codeContent}
`; + // Use highlighted content if available, otherwise use escaped content + result += `
${highlightedContent}
`; result += `
${closeFence}
`; return result;