]*>([\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;