├── .gitignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── WONT-DO.md ├── assets ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico └── site.webmanifest ├── build.js ├── contrib_docs └── RELEASE_PROCESS.md ├── demo.html ├── diagram-embed.html ├── diagram-original.html ├── diagram.png ├── dist ├── overtype.cjs ├── overtype.d.ts ├── overtype.esm.js ├── overtype.esm.js.map ├── overtype.js ├── overtype.js.map └── overtype.min.js ├── examples ├── basic.html ├── custom-theme.html ├── dynamic.html └── multiple.html ├── favicon.ico ├── index.html ├── logo-text.svg ├── open-graph.png ├── package-lock.json ├── package.json ├── src ├── icons.js ├── index.js ├── link-tooltip.js ├── overtype.d.ts ├── overtype.js ├── parser.js ├── shortcuts.js ├── styles.js ├── themes.js └── toolbar.js ├── test-types.ts └── test ├── api-methods.test.js ├── comprehensive-alignment.test.js ├── links.test.js ├── list-indentation.test.js ├── mode-switching.test.js ├── overtype.test.js ├── preview-mode.test.js ├── sanctuary-parsing.test.js └── smart-lists.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Development 5 | .DS_Store 6 | *.log 7 | *.swp 8 | .env 9 | .env.local 10 | 11 | # IDE 12 | .vscode/ 13 | .idea/ 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | # Build artifacts 18 | *.map 19 | 20 | # Testing 21 | coverage/ 22 | .nyc_output/ 23 | 24 | # Temporary files 25 | tmp/ 26 | temp/ 27 | *.tmp 28 | 29 | # OS files 30 | Thumbs.db -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # OverType Architecture Documentation 2 | 3 | ## Core Concept: The Transparent Overlay Technique 4 | 5 | OverType achieves perfect WYSIWYG markdown editing through a deceptively simple yet powerful technique: overlaying a transparent textarea on top of a styled preview div. This document provides a comprehensive overview of the architecture, enabling reimplementation from scratch. 6 | 7 | ## Fundamental Architecture 8 | 9 | ### The Two-Layer System 10 | 11 | The core innovation involves two perfectly aligned layers: 12 | 13 | 1. **Input Layer (textarea)**: Completely transparent text, handles all user input, cursor management, and selection 14 | 2. **Preview Layer (div)**: Styled markdown rendering positioned exactly beneath the textarea 15 | 16 | The breakthrough is maintaining **pixel-perfect alignment** between these layers through meticulous style synchronization: 17 | - Identical font properties (family, size, weight, variant, synthesis, kerning, ligatures) 18 | - Identical box model (padding, margin, border, box-sizing) 19 | - Identical text layout (white-space, word-wrap, word-break, tab-size, line-height, letter-spacing) 20 | - Identical positioning and dimensions (absolute positioning, matching width/height) 21 | - Identical overflow and scrolling behavior 22 | 23 | ### DOM Structure 24 | 25 | ``` 26 | .overtype-container 27 | ├── .overtype-toolbar (optional) 28 | ├── .overtype-wrapper 29 | │ ├── textarea.overtype-input 30 | │ └── div.overtype-preview 31 | └── .overtype-stats (optional) 32 | ``` 33 | 34 | The container uses CSS Grid for layout control, enabling proper toolbar and stats positioning while maintaining the core overlay functionality. 35 | 36 | ## Component Architecture 37 | 38 | ### 1. Main Controller (OverType Class) 39 | 40 | **Primary Responsibilities:** 41 | - Instance lifecycle management (creation, initialization, destruction) 42 | - DOM construction and structure recovery 43 | - Event orchestration and delegation 44 | - Mode switching (normal, plain, preview) 45 | - Global style injection and theme management 46 | - Multi-instance coordination 47 | 48 | **Key Architectural Patterns:** 49 | - **WeakMap Instance Tracking**: Prevents memory leaks while maintaining instance references 50 | - **Array Return Pattern**: Constructor always returns arrays for consistent multi-instance handling 51 | - **DOM Recovery**: Can reinitialize from existing DOM structures for page persistence 52 | - **Global Event Delegation**: Single document-level listeners manage all instances efficiently 53 | 54 | **Edge Cases Handled:** 55 | - Multiple instances on same page 56 | - Dynamic instance creation/destruction 57 | - Browser back/forward navigation persistence 58 | - Theme changes affecting all instances 59 | - Responsive behavior and mobile optimization 60 | 61 | ### 2. Markdown Parser 62 | 63 | **Primary Responsibilities:** 64 | - Character-aligned markdown-to-HTML conversion 65 | - XSS prevention and URL sanitization 66 | - Syntax marker preservation (not hiding markdown characters) 67 | - Post-processing for semantic HTML structure 68 | 69 | **Key Architectural Patterns:** 70 | - **Sanctuary Pattern**: Uses Unicode Private Use Area characters (U+E000-U+E001) as placeholders to protect parsed content during multi-pass processing 71 | - **Line-by-Line Processing**: Maintains line boundaries for proper alignment 72 | - **Syntax Marker Wrapping**: Markdown characters wrapped in `` for CSS-based hiding in preview mode 73 | - **Link Indexing**: Assigns unique anchor names for CSS anchor positioning 74 | 75 | **Processing Pipeline:** 76 | 1. HTML entity escaping 77 | 2. Protected region identification (URL portions of links) 78 | 3. Selective inline code protection (excluding URL regions) 79 | 4. Link sanctuary creation with preserved URLs 80 | 5. Bold/italic parsing (with word-boundary underscore handling) 81 | 6. Link restoration with separate text/URL processing 82 | 7. List item detection 83 | 8. Header transformation 84 | 9. Post-processing (list consolidation, code block formatting) 85 | 86 | **Protected Regions Strategy:** 87 | The parser uses a "protected regions" approach for URLs to prevent markdown processing: 88 | - First pass identifies all link URLs and marks their byte positions as protected 89 | - Inline code detection skips any patterns within protected regions 90 | - URLs are restored verbatim while link text receives full markdown processing 91 | - This ensures special characters in URLs (`**`, `_`, `` ` ``, `~`) remain literal 92 | 93 | **Edge Cases Handled:** 94 | - Nested formatting (bold within italic, links with formatting) 95 | - Underscores in code not triggering emphasis 96 | - URLs containing markdown characters (protected region strategy) 97 | - Underscores requiring word boundaries for italic (prevents `word_with_underscore` issues) 98 | - Mixed list types and indentation levels 99 | - Code blocks with markdown-like content 100 | - XSS attack vectors in URLs 101 | - Inline code within link text but not URLs 102 | 103 | ### 3. Style System 104 | 105 | **Primary Responsibilities:** 106 | - Critical alignment CSS generation 107 | - Theme application and custom property management 108 | - Mode-specific styling (edit, plain, preview) 109 | - Parent style isolation and defensive CSS 110 | 111 | **Key Architectural Patterns:** 112 | - **CSS Custom Properties**: All theme colors and instance settings as CSS variables 113 | - **Dynamic Style Injection**: Styles generated in JavaScript and injected once globally 114 | - **Instance-Specific Properties**: Per-editor customization through inline CSS variables 115 | - **High Specificity Defense**: Prevents parent styles from breaking alignment 116 | 117 | **Style Categories:** 118 | 1. **Alignment Critical**: Font, spacing, positioning properties that must match exactly 119 | 2. **Visual Enhancement**: Colors, backgrounds, borders for appearance 120 | 3. **Mode Specific**: Different rules for plain/preview/edit modes 121 | 4. **Mobile Optimization**: Touch-specific adjustments and responsive behavior 122 | 123 | **Edge Cases Handled:** 124 | - Parent CSS interference (aggressive resets) 125 | - Browser font rendering differences 126 | - Android monospace font issues 127 | - Safari elastic scroll desynchronization 128 | - High-DPI display rendering 129 | 130 | ### 4. Toolbar System 131 | 132 | **Primary Responsibilities:** 133 | - Markdown formatting action execution 134 | - Button state synchronization with cursor position 135 | - View mode switching (dropdown menu) 136 | - Keyboard shortcut coordination 137 | 138 | **Key Architectural Patterns:** 139 | - **Action Delegation**: Uses external markdown-actions library for formatting 140 | - **State Polling**: Updates button active states on selection changes 141 | - **Fixed Positioning Dropdowns**: Menus append to body to avoid clipping 142 | - **Icon System**: Inline SVG icons for zero external dependencies 143 | 144 | **Integration Points:** 145 | - Coordinates with shortcuts manager for consistent behavior 146 | - Updates on global selection change events 147 | - Triggers preview updates after actions 148 | - Manages focus restoration after button clicks 149 | 150 | ### 5. Event System 151 | 152 | **Global Event Strategy:** 153 | The architecture uses document-level event delegation for efficiency: 154 | 155 | 1. **Input Events**: Single listener handles all textarea inputs across instances 156 | 2. **Selection Changes**: Debounced global listener updates toolbar and stats 157 | 3. **Keyboard Events**: Keydown captured for shortcuts and special behaviors 158 | 4. **Scroll Synchronization**: Ensures preview scrolls with textarea 159 | 160 | **Event Flow:** 161 | 1. User action triggers browser event 162 | 2. Global listener identifies affected instance 163 | 3. Instance method processes event 164 | 4. Update cycle triggered if needed 165 | 5. Callbacks fired for external integration 166 | 167 | **Debouncing Strategy:** 168 | - Selection changes: 50ms delay 169 | - Preview updates: Synchronous on input 170 | - Stats updates: Throttled to 100ms 171 | - Toolbar state: 50ms after selection 172 | 173 | ### 6. Update Cycle 174 | 175 | **Primary Update Flow:** 176 | 1. User types in textarea 177 | 2. Input event triggered 178 | 3. Parser converts markdown to HTML 179 | 4. Preview innerHTML updated 180 | 5. Special processing applied (code backgrounds) 181 | 6. Stats recalculated 182 | 7. onChange callback fired 183 | 184 | **Optimization Strategies:** 185 | - Active line detection for partial updates 186 | - Cached parser results for unchanged content 187 | - Batched DOM updates 188 | - Deferred non-critical updates 189 | 190 | ### 7. View Modes 191 | 192 | **Three Distinct Modes:** 193 | 194 | 1. **Normal Mode**: Standard overlay editing 195 | - Transparent textarea visible 196 | - Preview shows styled markdown 197 | - Syntax markers visible 198 | 199 | 2. **Plain Mode**: Raw markdown editing 200 | - Preview hidden 201 | - Textarea fully visible 202 | - System font for true plain text 203 | 204 | 3. **Preview Mode**: Read-only rendering 205 | - Textarea hidden 206 | - Preview interactive (clickable links) 207 | - Syntax markers hidden via CSS 208 | - Typography enhanced 209 | 210 | **Mode Switching:** 211 | - CSS class-based switching 212 | - Minimal JavaScript involvement 213 | - Instant visual feedback 214 | - State preservation across switches 215 | 216 | ## Reusable Patterns 217 | 218 | ### Instance Management Pattern 219 | Every component that needs instance tracking uses WeakMap to prevent memory leaks while maintaining references. This pattern appears in the main class, toolbar, and tooltip systems. 220 | 221 | ### Sanctuary Protection Pattern 222 | Complex multi-pass parsing uses placeholder characters from Unicode Private Use Area to protect already-parsed content. This prevents recursive parsing and maintains content integrity. 223 | 224 | ### CSS Variable Integration Pattern 225 | All customizable values flow through CSS custom properties, enabling both global and instance-specific theming without JavaScript style manipulation. 226 | 227 | ### Event Delegation Pattern 228 | Instead of instance-specific listeners, global document listeners identify affected instances through DOM traversal, reducing memory usage and simplifying cleanup. 229 | 230 | ### Post-Processing Pattern 231 | Raw parser output undergoes post-processing for semantic HTML structure (like list consolidation), separating parsing concerns from presentation. 232 | 233 | ## Critical Implementation Details 234 | 235 | ### Achieving Perfect Alignment 236 | 237 | The most critical aspect is ensuring the textarea and preview div render text identically: 238 | 239 | 1. **Font Stack Matching**: Both elements must use identical font families, including fallbacks 240 | 2. **Font Synthesis Control**: Disable synthetic bold/italic to prevent width changes 241 | 3. **Ligature Disabling**: Prevents character combinations from changing widths 242 | 4. **Whitespace Handling**: Both must handle spaces, tabs, and line breaks identically 243 | 5. **Box Model Alignment**: Padding, borders, and margins must match exactly 244 | 6. **Scroll Synchronization**: Overflow and scroll positions must stay locked 245 | 246 | ### Browser-Specific Considerations 247 | 248 | **Chrome/Edge:** 249 | - Generally most consistent 250 | - Supports all modern features 251 | - CSS anchor positioning works 252 | 253 | **Firefox:** 254 | - Requires explicit font-synthesis settings 255 | - Different selection event timing 256 | 257 | **Safari:** 258 | - Elastic scrolling can cause desync 259 | - Requires special touch handling 260 | - Font rendering differs slightly 261 | 262 | **Mobile (iOS/Android):** 263 | - 16px minimum font prevents zoom 264 | - Touch-action manipulation needed 265 | - Android monospace font issues 266 | - Virtual keyboard handling 267 | 268 | ### Performance Optimization 269 | 270 | **Parsing Performance:** 271 | - Line-by-line processing limits scope 272 | - Sanctuary pattern prevents re-parsing 273 | - Simple regex over complex parsing 274 | 275 | **DOM Performance:** 276 | - innerHTML updates over incremental changes 277 | - Batched updates where possible 278 | - Deferred non-critical updates 279 | 280 | **Memory Management:** 281 | - WeakMap for instance tracking 282 | - Event delegation over instance listeners 283 | - Cleanup on destroy 284 | 285 | ## Extension Points 286 | 287 | ### Custom Themes 288 | Themes are objects with color definitions that convert to CSS custom properties. The system supports: 289 | - Complete theme definitions 290 | - Partial overrides 291 | - Per-instance themes 292 | - Dynamic theme switching 293 | 294 | ### Toolbar Customization 295 | The toolbar accepts button configurations with: 296 | - Custom icons (SVG strings) 297 | - Action callbacks 298 | - State detection functions 299 | - Dropdown menus 300 | 301 | ### Parser Extensions 302 | The parser can be extended by: 303 | - Adding new sanctuary types 304 | - Introducing new syntax patterns 305 | - Modifying post-processing 306 | - Custom HTML generation 307 | 308 | ### Event Hooks 309 | Integration points include: 310 | - onChange callbacks 311 | - onKeydown intercepts 312 | - Custom stats formatters 313 | - Initialization callbacks 314 | 315 | ## Security Considerations 316 | 317 | ### XSS Prevention 318 | - All user content HTML-escaped before parsing 319 | - URL sanitization blocks dangerous protocols 320 | - No eval or innerHTML of user content 321 | - Sanctuary system prevents injection during parsing 322 | 323 | ### Content Security 324 | - Links use data-href in edit mode 325 | - Preview mode enables links safely 326 | - No external resource loading 327 | - Self-contained implementation 328 | 329 | ## Testing Strategy 330 | 331 | ### Critical Test Areas 332 | 333 | 1. **Alignment Tests**: Verify pixel-perfect overlay alignment 334 | 2. **Parser Tests**: Comprehensive markdown edge cases 335 | 3. **XSS Tests**: Security vulnerability testing 336 | 4. **Performance Tests**: Large document handling 337 | 5. **Browser Tests**: Cross-browser compatibility 338 | 6. **Mobile Tests**: Touch and responsive behavior 339 | 340 | ### Test Patterns 341 | - Unit tests for parser functions 342 | - Integration tests for update cycle 343 | - E2E tests for user interactions 344 | - Performance benchmarks for large documents 345 | - Security audits for XSS vectors 346 | 347 | ## Common Pitfalls and Solutions 348 | 349 | ### Pitfall: Font Rendering Differences 350 | **Solution**: Comprehensive font stack with explicit fallbacks and synthesis control 351 | 352 | ### Pitfall: Parent Style Interference 353 | **Solution**: Aggressive CSS reset with high specificity and !important flags 354 | 355 | ### Pitfall: Scroll Desynchronization 356 | **Solution**: Explicit scroll event handling and position synchronization 357 | 358 | ### Pitfall: Mobile Keyboard Issues 359 | **Solution**: Viewport management and resize detection 360 | 361 | ### Pitfall: Memory Leaks 362 | **Solution**: WeakMap references and proper cleanup on destroy 363 | 364 | ## Implementation Checklist 365 | 366 | To reimplement OverType from scratch: 367 | 368 | 1. ✅ Create two-layer DOM structure with absolute positioning 369 | 2. ✅ Implement style synchronization system 370 | 3. ✅ Build line-by-line markdown parser with sanctuary pattern 371 | 4. ✅ Add post-processing for semantic HTML 372 | 5. ✅ Implement global event delegation system 373 | 6. ✅ Create theme system with CSS variables 374 | 7. ✅ Add toolbar with markdown-actions integration 375 | 8. ✅ Implement view mode switching 376 | 9. ✅ Add XSS protection and URL sanitization 377 | 10. ✅ Handle browser-specific edge cases 378 | 11. ✅ Optimize for mobile devices 379 | 12. ✅ Add destroy and cleanup methods 380 | 13. ✅ Implement auto-resize functionality 381 | 14. ✅ Add comprehensive error handling 382 | 383 | ## Conclusion 384 | 385 | OverType's architecture demonstrates that complex WYSIWYG editing can be achieved through elegant simplicity. The transparent overlay technique, combined with careful style synchronization and smart parsing, creates a powerful yet maintainable editor. The modular architecture allows for extension while the core concept remains simple: two perfectly aligned layers creating the illusion of styled text input. 386 | 387 | The key insight is that by accepting the constraint of monospace fonts and exact alignment, we can avoid the complexity of contentEditable while providing a superior editing experience. This architecture proves that sometimes the best solution is not to fight the browser but to work with its native capabilities in creative ways. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to OverType will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.2.7] - 2025-09-30 11 | 12 | ### Fixed 13 | - **Issue #55: Double-escaping of HTML entities in code blocks** - HTML special characters (`<`, `>`, `&`, `"`) inside inline code spans are now properly escaped once instead of twice 14 | - Removed redundant `escapeHtml()` calls when rendering code sanctuaries 15 | - Fixes issue where `` `` `` would display as `&lt;angle brackets&gt;` instead of `<angle brackets>` 16 | - Also fixed the same issue for inline code within link text 17 | - Thanks to [@lyricat](https://github.com/lyricat) for identifying and fixing this issue (PR #56) 18 | 19 | ### Added 20 | - Comprehensive test suite for HTML entity escaping in code blocks 21 | 22 | ## [1.2.6] - 2025-09-08 23 | 24 | ### Fixed 25 | - **Re-enabled code button inside links** - Now that the sanctuary pattern properly handles inline code within link text, the code button works correctly without Unicode placeholder issues 26 | - **Removed unnecessary code** - Deleted the `isInsideLink` function that was no longer needed, reducing bundle size 27 | 28 | ### Changed 29 | - **README update** - Replaced Synesthesia section with Hyperclay information 30 | 31 | ## [1.2.5] - 2025-09-08 32 | 33 | ### Fixed 34 | - **URL formatting protection** - Markdown formatting characters in URLs are now preserved as literal text 35 | - Implemented "protected regions" strategy for URL portions of links 36 | - Backticks, asterisks, underscores, and tildes in URLs remain unchanged 37 | - Link text can still contain formatted content (bold, italic, code, etc.) 38 | - Fixes issue where `[Link](https://example.com/`path`/file)` would break the URL 39 | - **Italic underscore handling** - Underscores now require word boundaries for italic formatting 40 | - Prevents false matches in words like `bold_with_underscore` 41 | - Single underscores only create italic at word boundaries 42 | 43 | ### Added 44 | - Comprehensive sanctuary parsing test suite for URL protection 45 | - Release process documentation in contrib_docs/ 46 | 47 | ## [1.2.4] - 2025-09-04 48 | 49 | ### Fixed 50 | - **Issue #48: Code formatting inside links** - Code button now disabled when cursor is inside a link 51 | - Added `isInsideLink()` detection to toolbar to prevent placeholder issues 52 | - Prevents Unicode placeholders from appearing when trying to format code within link text 53 | - **Issue #47: Tailwind CSS animation conflict** - Renamed keyframe to avoid clashes 54 | - Changed `@keyframes pulse` to `@keyframes overtype-pulse` 55 | - Fixes conflict with Tailwind's `animate-pulse` utility class 56 | - **Issue #45: HTML output methods confusion** - Methods now have distinct purposes 57 | - `getRenderedHTML()` returns HTML with syntax markers (for debugging) 58 | - `getRenderedHTML({ cleanHTML: true })` returns clean HTML without OverType markup 59 | - `getCleanHTML()` added as convenience alias for clean HTML 60 | - `getPreviewHTML()` returns actual DOM content from preview layer 61 | - **Issue #43: TypeScript support** - Added comprehensive TypeScript definitions 62 | - TypeScript definitions included in package (`dist/overtype.d.ts`) 63 | - Added `types` field to package.json 64 | - Definitions automatically tested during build process 65 | - Full type support for all OverType features including themes, options, and methods 66 | - **Toolbar configuration** - Made toolbar button config more robust 67 | - Fixed missing semicolon in toolbar.js 68 | - Added proper fallback for undefined buttonConfig 69 | 70 | ### Added 71 | - TypeScript definition testing integrated into build process 72 | - `test-types.ts` validates all type definitions 73 | - Build fails if TypeScript definitions have errors 74 | - Added `test:types` npm script for standalone testing 75 | 76 | ### Changed 77 | - Link tooltip styles now use `!important` to prevent CSS reset overrides 78 | - Ensures tooltip remains visible even with aggressive parent styles 79 | 80 | ## [1.2.3] - 2025-08-23 81 | 82 | ### Added 83 | - **Smart List Continuation** (Issue #26) - GitHub-style automatic list continuation 84 | - Press Enter at the end of a list item to create a new one 85 | - Press Enter on an empty list item to exit the list 86 | - Press Enter in the middle of text to split it into two items 87 | - Supports bullet lists (`-`, `*`, `+`), numbered lists, and checkboxes 88 | - Numbered lists automatically renumber when items are added or removed 89 | - Enabled by default with `smartLists: true` option 90 | 91 | ## [1.2.2] - 2025-08-23 92 | 93 | ### Fixed 94 | - **Issue #32: Alignment problems with tables and code blocks** 95 | - Code fences (```) are now preserved and visible in the preview 96 | - Content inside code blocks is no longer parsed as markdown 97 | - Used semantic `
` blocks while keeping fences visible
 98 | - **Fixed double-escaping of HTML entities in code blocks**
 99 |   - Changed from using `innerHTML` to `textContent` when extracting code block content
100 |   - Removed unnecessary text manipulation in `_applyCodeBlockBackgrounds()`
101 |   - Special characters like `>`, `<`, `&` now display correctly in code blocks
102 | 
103 | ## [1.2.1] - 2025-08-23
104 | 
105 | ### Fixed
106 | - Tab indentation can now be properly undone with Ctrl/Cmd+Z
107 |   - Previously, tabbing operations were not tracked in the undo history
108 |   - Users can now undo/redo tab insertions and multi-line indentations
109 | 
110 | ## [1.2.0] - 2025-08-21
111 | 
112 | ### Added
113 | - **View Modes** - Three distinct editing/viewing modes accessible via toolbar dropdown
114 |   - Normal Edit Mode: Default WYSIWYG markdown editing with syntax highlighting
115 |   - Plain Textarea Mode: Shows raw markdown without preview overlay  
116 |   - Preview Mode: Read-only rendered preview with proper typography and clickable links
117 | - **API Methods for HTML Export**
118 |   - `getRenderedHTML(processForPreview)`: Get rendered HTML of current content
119 |   - `getPreviewHTML()`: Get the exact HTML displayed in preview layer
120 |   - Enables external preview generation and HTML export functionality
121 | - **View Mode API Methods**
122 |   - `showPlainTextarea(boolean)`: Programmatically switch to/from plain textarea mode
123 |   - `showPreviewMode(boolean)`: Programmatically switch to/from preview mode
124 | - **Enhanced Link Handling**
125 |   - Links now always have real hrefs (pointer-events controls clickability)
126 |   - Links properly hidden in preview mode (no more visible `](url)` syntax)
127 |   - Simplified implementation without dynamic href updates
128 | - **CSS Isolation Improvements**
129 |   - Middle-ground CSS reset prevents parent styles from leaking into editor
130 |   - Protects against inherited margins, padding, borders, and decorative styles
131 |   - Maintains proper inheritance for fonts and colors
132 | - **Dropdown Menu System**
133 |   - Fixed positioning dropdown menus that work with scrollable toolbar
134 |   - Dropdown appends to document.body to avoid overflow clipping
135 |   - Proper z-index management for reliable visibility
136 | - **Comprehensive Test Suite**
137 |   - Added tests for preview mode functionality
138 |   - Added tests for link parsing and XSS prevention
139 |   - Added tests for new API methods (getValue, getRenderedHTML, getPreviewHTML)
140 |   - Test coverage includes view mode switching, HTML rendering, and post-processing
141 | 
142 | ### Fixed
143 | - **Preview Mode Link Rendering** - URL syntax parts now properly hidden in preview mode
144 | - **Code Block Backgrounds** - Restored pale yellow background in normal mode
145 | - **Dropdown Menu Positioning** - Fixed dropdown being cut off by toolbar overflow
146 | - **Cave Theme Styling**
147 |   - Eye icon button now has proper contrast when active (dropdown-active state)
148 |   - Code blocks in preview mode use appropriate dark background (#11171F)
149 | - **Toolbar Scrolling** - Toolbar now scrolls horizontally on all screen sizes as intended
150 | - **CSS Conflicts** - Parent page styles no longer interfere with editor styling
151 | 
152 | ### Changed
153 | - Link implementation simplified - always uses real hrefs with CSS controlling interaction
154 | - Post-processing for lists and code blocks now works in both browser and Node.js environments
155 | - Toolbar overflow changed from hidden to auto for horizontal scrolling
156 | - Dropdown menus use fixed positioning instead of absolute
157 | - **Removed `overscroll-behavior: none`** to restore scroll-through behavior
158 |   - Users can now continue scrolling the parent page when reaching editor boundaries
159 |   - Trade-off: Minor visual desync during Safari elastic bounce vs trapped scrolling
160 | 
161 | ## [1.1.8] - 2025-01-20
162 | 
163 | ### Fixed
164 | - Android bold/italic rendering regression from v1.1.3
165 |   - Removed `font-synthesis: none` to restore synthetic bold/italic on Android devices
166 |   - Updated font stack to avoid 'ui-monospace' pitfalls while maintaining Android support
167 |   - Font stack now properly includes: SF Mono, Roboto Mono, Noto Sans Mono, Droid Sans Mono
168 |   - Fixes issue where Android users could not see bold or italic text formatting
169 | 
170 | ## [1.1.7] - 2025-01-20
171 | 
172 | ### Security
173 | - Fixed XSS vulnerability where javascript: protocol links could execute arbitrary code (#25)
174 |   - Added URL sanitization to block dangerous protocols (javascript:, data:, vbscript:, etc.)
175 |   - Safe protocols allowed: http://, https://, mailto:, ftp://, ftps://
176 |   - Relative URLs and hash links continue to work normally
177 |   - Dangerous URLs are neutralized to "#" preventing code execution
178 | 
179 | ## [1.1.6] - 2025-01-20
180 | 
181 | ### Fixed
182 | - URLs with markdown characters (underscores, asterisks) no longer break HTML structure (#23)
183 |   - Implemented "URL Sanctuary" pattern to protect link URLs from markdown processing
184 |   - Links are now treated as protected zones where markdown syntax is literal text
185 |   - Fixes malformed HTML when URLs contain `_`, `__`, `*`, `**` characters
186 |   - Preserves proper href attributes and visual rendering
187 | 
188 | ## [1.1.5] - 2025-01-20
189 | 
190 | ### Added
191 | - TypeScript definitions file (`src/overtype.d.ts`) with complete type definitions (#20)
192 | - TypeScript test file (`test-types.ts`) for type validation
193 | 
194 | ### Fixed
195 | - Text selection desynchronization during overscroll on browsers with elastic scrolling (#17)
196 |   - Added `overscroll-behavior: none` to prevent bounce animation at scroll boundaries
197 |   - Ensures text selection stays synchronized between textarea and preview layers
198 | 
199 | ## [1.1.4] - 2025-01-19
200 | 
201 | ### Fixed
202 | - Code blocks no longer render markdown formatting - `__init__` displays correctly (#14)
203 |   - Post-processing strips all formatting from lines inside code blocks
204 |   - Preserves plain text display for asterisks, underscores, backticks, etc.
205 | 
206 | ## [1.1.3] - 2025-01-19
207 | 
208 | ### Fixed
209 | - Inline triple backticks no longer mistaken for code blocks (#15)
210 |   - Code fences now only recognized when alone on a line or followed by language identifier
211 |   - Prevents cascade failures where inline backticks break subsequent code blocks
212 | - Android cursor misalignment on bold text (#16)
213 |   - Updated font stack to avoid problematic `ui-monospace` on Android
214 |   - Added explicit Android fonts: Roboto Mono, Noto Sans Mono, Droid Sans Mono
215 |   - Added `font-synthesis: none` and `font-variant-ligatures: none` to prevent width drift
216 | 
217 | ## [1.1.2] - 2025-01-19
218 | 
219 | ### Added
220 | - `textareaProps` option to pass native HTML attributes to textarea (required, maxLength, name, etc.) (#8)
221 | - `autoResize` option for auto-expanding editor height based on content
222 | - `minHeight` and `maxHeight` options for controlling auto-resize bounds
223 | - Form integration example in README showing how to use with HTML form validation
224 | 
225 | ### Fixed
226 | - Height issue when toolbar and stats bar are enabled - container now uses CSS Grid properly (#9)
227 | - Grid layout issue where editors without toolbars would collapse to min-height
228 | - Added explicit grid-row positions for toolbar, wrapper, and stats elements
229 | - Stats bar now positioned at bottom of container using grid (not absolute positioning)
230 | 
231 | ### Changed
232 | - Container uses CSS Grid layout (`grid-template-rows: auto 1fr auto`) for proper height distribution
233 | - Toolbar takes auto height, editor wrapper takes remaining space (1fr), stats bar takes auto height
234 | - Bundle size: 60.89 KB minified (16.8 KB gzipped)
235 | 
236 | ## [1.1.1] - 2025-01-18
237 | 
238 | ### Changed
239 | - Link tooltips now use CSS Anchor Positioning for perfect placement
240 | - Tooltips position directly below the rendered link text (not approximated)
241 | - Removed Floating UI dependency, reducing bundle size from 73KB to 59KB minified
242 | - Parser now adds anchor names to rendered links for CSS positioning
243 | - Demo page redesigned to match dark terminal aesthetic
244 | - Added "SEE ALL DEMOS" button to index.html
245 | 
246 | ### Fixed
247 | - Link tooltip positioning now accurate relative to rendered text
248 | 
249 | ## [1.1.0] - 2025-01-18
250 | 
251 | ### Added
252 | - Gmail/Google Docs style link tooltips - cursor in link shows clickable URL tooltip (#4)
253 | - Tab key support - inserts 2 spaces, supports multi-line indent/outdent with Shift+Tab (#3)
254 | - Comprehensive "Limitations" section in README documenting design constraints (#5)
255 | - @floating-ui/dom dependency for tooltip positioning
256 | 
257 | ### Fixed
258 | - Inline code with underscores/asterisks no longer incorrectly formatted (#2, PR #6 by @joshdoman)
259 | - Code elements now properly inherit font-size, preventing alignment breaks (#1)
260 | - Tab key no longer causes focus loss and cursor misalignment (#3)
261 | 
262 | ### Changed
263 | - Links now use tooltip interaction instead of Cmd/Ctrl+Click (better UX)
264 | - README limitations section moved below Examples for better flow
265 | - Build size increased to 73KB minified (from 45KB) due to Floating UI library
266 | 
267 | ### Contributors
268 | - Josh Doman (@joshdoman) - Fixed inline code formatting preservation
269 | 
270 | ## [1.0.6] - 2024-08-17
271 | 
272 | ### Added
273 | - Initial public release on Hacker News
274 | - Core transparent textarea overlay functionality
275 | - Optional toolbar with markdown formatting buttons
276 | - Keyboard shortcuts for common markdown operations
277 | - Solar (light) and Cave (dark) themes
278 | - DOM persistence and recovery
279 | - Mobile optimization
280 | - Stats bar showing word/character count
281 | 
282 | ### Features at Launch
283 | - 👻 Invisible textarea overlay for seamless editing
284 | - 🎨 Global theming system
285 | - ⌨️ Keyboard shortcuts (Cmd/Ctrl+B for bold, etc.)
286 | - 📱 Mobile optimized with responsive design
287 | - 🔄 DOM persistence aware (works with HyperClay)
288 | - 🚀 Lightweight ~45KB minified
289 | - 🎯 Optional toolbar
290 | - ✨ Smart shortcuts with selection preservation
291 | - 🔧 Framework agnostic
292 | 
293 | [1.1.5]: https://github.com/panphora/overtype/compare/v1.1.4...v1.1.5
294 | [1.1.4]: https://github.com/panphora/overtype/compare/v1.1.3...v1.1.4
295 | [1.1.3]: https://github.com/panphora/overtype/compare/v1.1.2...v1.1.3
296 | [1.1.2]: https://github.com/panphora/overtype/compare/v1.1.1...v1.1.2
297 | [1.1.1]: https://github.com/panphora/overtype/compare/v1.1.0...v1.1.1
298 | [1.1.0]: https://github.com/panphora/overtype/compare/v1.0.6...v1.1.0
299 | [1.0.6]: https://github.com/panphora/overtype/releases/tag/v1.0.6


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2025 David Miranda
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.


--------------------------------------------------------------------------------
/WONT-DO.md:
--------------------------------------------------------------------------------
 1 | # OverType: Features We Won't Implement
 2 | 
 3 | This document outlines features that are intentionally excluded from OverType to maintain its core philosophy of simplicity, performance, and perfect alignment.
 4 | 
 5 | ## Open to Elegant Solutions
 6 | 
 7 | These features could be considered if someone proposes a performant, elegant solution that doesn't break existing functionality:
 8 | 
 9 | ### Syntax Highlighting in Code Blocks
10 | **Why it's challenging:**
11 | - Multi-line tokens can span across OverType's line-by-line DOM structure
12 | - Each line is in a separate `
` element 13 | - Syntax highlighters expect continuous text blocks 14 | - Would need to maintain character-perfect alignment 15 | 16 | **What we'd need:** 17 | - Solution that works with line-by-line DOM structure 18 | - No impact on editing performance 19 | - Maintains perfect character alignment 20 | - Lightweight (< 10KB added) 21 | - Works with existing architecture 22 | 23 | --- 24 | 25 | ## Will Not Implement 26 | 27 | These features are fundamentally incompatible with OverType's design philosophy and architecture: 28 | 29 | ### 🚫 Images 30 | **Why not:** 31 | - Images break the character grid alignment 32 | - Would require placeholder characters or break the overlay model 33 | - Many excellent rich editors already handle images well 34 | 35 | **Alternative:** Use OverType for text editing, preview rendered markdown elsewhere for images. 36 | 37 | ### 🚫 Tables 38 | **Why not:** 39 | - Variable column widths break monospace grid assumptions 40 | - Table navigation (Tab between cells) conflicts with textarea behavior 41 | - Would require significant architecture changes 42 | 43 | **Alternative:** Use formatted code blocks for simple ASCII tables. 44 | 45 | ### 🚫 Auto-complete / IntelliSense 46 | **Why not:** 47 | - Popup menus break the overlay alignment 48 | - Conflicts with native mobile keyboards and accessibility tools 49 | - Goes against the "transparent textarea" philosophy 50 | 51 | **Alternative:** Use native browser/OS autocomplete and spell-check features. 52 | 53 | ### 🚫 Split Pane Preview 54 | **Why not:** 55 | - OverType IS the preview (that's the whole point) 56 | - Split pane defeats the overlay innovation 57 | - Plenty of other editors offer split preview 58 | 59 | **Alternative:** Use the proposed preview mode toggle for a clean reading view. 60 | 61 | ### 🚫 File Tree / Project Management 62 | **Why not:** 63 | - OverType is an editor component, not an IDE 64 | - File management is the host application's responsibility 65 | - Would massively expand scope 66 | 67 | **Alternative:** Integrate OverType into existing IDEs or editors that have file management. 68 | 69 | ### 🚫 Vim/Emacs Keybindings 70 | **Why not:** 71 | - Modal editing breaks textarea native behavior 72 | - Complexity explosion for minority use case 73 | - Better served by actual Vim/Emacs or plugins 74 | 75 | **Alternative:** Use browser extensions that add Vim keybindings to all textareas. 76 | 77 | --- 78 | 79 | ## Design Philosophy 80 | 81 | OverType intentionally stays small and focused. Every feature has a cost in: 82 | - **Code complexity** - More code means more bugs 83 | - **Performance** - Every feature adds overhead 84 | - **Maintenance** - Features need updates and bug fixes forever 85 | - **Learning curve** - More features make it harder to understand 86 | - **Testing surface** - More combinations to test 87 | 88 | By saying "no" to these features, OverType can remain: 89 | - **Fast** - Instant response, no lag 90 | - **Small** - ~60KB minified 91 | - **Reliable** - Fewer features = fewer bugs 92 | - **Understandable** - You can read the entire source in an hour 93 | - **Maintainable** - One person can maintain it 94 | 95 | -------------------------------------------------------------------------------- /assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/assets/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/assets/favicon.ico -------------------------------------------------------------------------------- /assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { execSync } from 'child_process'; 5 | 6 | // Read package.json for version 7 | const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 8 | const version = packageJson.version; 9 | 10 | // Banner for all builds 11 | const banner = `/** 12 | * OverType v${version} 13 | * A lightweight markdown editor library with perfect WYSIWYG alignment 14 | * @license MIT 15 | * @author Demo User 16 | * https://github.com/demo/overtype 17 | */`; 18 | 19 | // Base configuration 20 | const baseConfig = { 21 | bundle: true, 22 | sourcemap: true, 23 | target: ['es2020', 'chrome62', 'firefox78', 'safari16'], 24 | banner: { 25 | js: banner 26 | }, 27 | loader: { 28 | '.js': 'js' 29 | }, 30 | // Prefer ESM versions of packages when available 31 | mainFields: ['module', 'main'] 32 | }; 33 | 34 | // Check for watch mode 35 | const isWatch = process.argv.includes('--watch'); 36 | const iifeBaseConfig = { 37 | ...baseConfig, 38 | format: 'iife', 39 | globalName: 'OverType', 40 | platform: 'browser', 41 | footer: { 42 | js: ` 43 | if (typeof window !== "undefined" && typeof window.document !== "undefined") { 44 | window.OverType = OverType.default ? OverType.default : OverType; 45 | } 46 | ` 47 | } 48 | }; 49 | async function build() { 50 | try { 51 | // Clean dist directory 52 | if (fs.existsSync('dist')) { 53 | fs.rmSync('dist', { recursive: true }); 54 | } 55 | fs.mkdirSync('dist'); 56 | 57 | if (isWatch) { 58 | // Development build with watch mode 59 | const ctx = await esbuild.context({ 60 | ...baseConfig, 61 | entryPoints: ['src/overtype.js'], 62 | outfile: 'dist/overtype.js', 63 | ...iifeBaseConfig, 64 | logLevel: 'info' 65 | }); 66 | 67 | await ctx.watch(); 68 | console.log('✅ Watching for changes...'); 69 | } else { 70 | // Browser IIFE Build (Development) 71 | await esbuild.build({ 72 | ...baseConfig, 73 | entryPoints: ['src/overtype.js'], 74 | outfile: 'dist/overtype.js', 75 | ...iifeBaseConfig 76 | }); 77 | console.log('✅ Built dist/overtype.js'); 78 | 79 | // Browser IIFE Build (Minified) 80 | await esbuild.build({ 81 | ...baseConfig, 82 | entryPoints: ['src/overtype.js'], 83 | outfile: 'dist/overtype.min.js', 84 | ...iifeBaseConfig, 85 | minify: true, 86 | sourcemap: false, 87 | }); 88 | console.log('✅ Built dist/overtype.min.js'); 89 | 90 | // CommonJS Build (for Node.js) 91 | await esbuild.build({ 92 | ...baseConfig, 93 | entryPoints: ['src/overtype.js'], 94 | outfile: 'dist/overtype.cjs', 95 | format: 'cjs', 96 | platform: 'node' 97 | }); 98 | console.log('✅ Built dist/overtype.cjs'); 99 | 100 | // ESM Build (for modern bundlers) 101 | await esbuild.build({ 102 | ...baseConfig, 103 | entryPoints: ['src/overtype.js'], 104 | outfile: 'dist/overtype.esm.js', 105 | format: 'esm', 106 | platform: 'browser' 107 | }); 108 | console.log('✅ Built dist/overtype.esm.js'); 109 | 110 | // Report sizes 111 | const iifeSize = fs.statSync('dist/overtype.js').size; 112 | const minSize = fs.statSync('dist/overtype.min.js').size; 113 | const cjsSize = fs.statSync('dist/overtype.cjs').size; 114 | const esmSize = fs.statSync('dist/overtype.esm.js').size; 115 | 116 | console.log('\n📊 Build sizes:'); 117 | console.log(` IIFE (Browser): ${(iifeSize / 1024).toFixed(2)} KB`); 118 | console.log(` IIFE Minified: ${(minSize / 1024).toFixed(2)} KB`); 119 | console.log(` CommonJS: ${(cjsSize / 1024).toFixed(2)} KB`); 120 | console.log(` ESM: ${(esmSize / 1024).toFixed(2)} KB`); 121 | 122 | // Update HTML files with actual minified size 123 | updateFileSizes(minSize); 124 | 125 | // Test TypeScript definitions before copying 126 | const typesSource = path.join(process.cwd(), 'src', 'overtype.d.ts'); 127 | const typesDest = path.join(process.cwd(), 'dist', 'overtype.d.ts'); 128 | if (fs.existsSync(typesSource)) { 129 | // Test the TypeScript definitions 130 | console.log('🔍 Testing TypeScript definitions...'); 131 | try { 132 | execSync('npx tsc --noEmit test-types.ts', { stdio: 'inherit' }); 133 | console.log('✅ TypeScript definitions test passed'); 134 | } catch (error) { 135 | console.error('❌ TypeScript definitions test failed'); 136 | console.error(' Run "npx tsc --noEmit test-types.ts" to see the errors'); 137 | process.exit(1); 138 | } 139 | 140 | // Copy to dist after successful test 141 | fs.copyFileSync(typesSource, typesDest); 142 | console.log('✅ Copied TypeScript definitions to dist/overtype.d.ts'); 143 | } 144 | 145 | console.log('\n✨ Build complete!'); 146 | } 147 | } catch (error) { 148 | console.error('❌ Build failed:', error); 149 | process.exit(1); 150 | } 151 | } 152 | 153 | // Function to update file sizes in HTML files 154 | function updateFileSizes(minifiedSize) { 155 | const sizeInKB = Math.round(minifiedSize / 1024); 156 | const sizeText = `${sizeInKB}KB`; 157 | 158 | // List of files to update 159 | const htmlFiles = ['index.html']; // Removed demo.html since it contains textarea content 160 | const markdownFiles = ['README.md']; 161 | 162 | // Update HTML files with span tags 163 | htmlFiles.forEach(file => { 164 | const filePath = path.join(process.cwd(), file); 165 | if (fs.existsSync(filePath)) { 166 | let content = fs.readFileSync(filePath, 'utf8'); 167 | 168 | // Replace all instances of file size within the special class 169 | // Pattern matches spans with class="overtype-size" and updates their content 170 | content = content.replace( 171 | /[\d~]+KB<\/span>/g, 172 | `${sizeText}` 173 | ); 174 | 175 | fs.writeFileSync(filePath, content, 'utf8'); 176 | console.log(` Updated ${file} with size: ${sizeText}`); 177 | } 178 | }); 179 | 180 | // Update markdown files (README.md) - replace ~XXkB patterns 181 | markdownFiles.forEach(file => { 182 | const filePath = path.join(process.cwd(), file); 183 | if (fs.existsSync(filePath)) { 184 | let content = fs.readFileSync(filePath, 'utf8'); 185 | 186 | // Replace size mentions in README - match patterns like ~45KB or 45KB 187 | content = content.replace( 188 | /~?\d+KB minified/g, 189 | `~${sizeText} minified` 190 | ); 191 | 192 | // Also update the comparison table 193 | content = content.replace( 194 | /\| \*\*Size\*\* \| ~?\d+KB \|/g, 195 | `| **Size** | ~${sizeText} |` 196 | ); 197 | 198 | fs.writeFileSync(filePath, content, 'utf8'); 199 | console.log(` Updated ${file} with size: ~${sizeText}`); 200 | } 201 | }); 202 | } 203 | 204 | // Run build 205 | build(); -------------------------------------------------------------------------------- /contrib_docs/RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # Release Process for OverType 2 | 3 | ## Pre-Release Checklist 4 | 5 | ### 1. Check for Temporary Files 6 | **IMPORTANT: Do this first before any other release steps** 7 | 8 | Run `git status` and check for uncommitted files that might be temporary: 9 | - Test files in root directory (test-*.js, test-*.html) 10 | - Debug files 11 | - Temporary documentation (FINAL_*, TEMP_*, etc.) 12 | - Personal test HTML files 13 | 14 | If temporary files are found: 15 | - Review each file 16 | - Delete temporary/debug files 17 | - Move useful tests to test/ directory 18 | - Report back what was cleaned up 19 | 20 | ### 2. Update Version 21 | - Update version in package.json 22 | - Follow semantic versioning (major.minor.patch) 23 | 24 | ### 3. Update CHANGELOG.md 25 | - Add new version section with date 26 | - List all changes, fixes, and features 27 | - Credit contributors if applicable 28 | 29 | ### 4. Run Tests 30 | ```bash 31 | npm test 32 | ``` 33 | All tests must pass before proceeding. 34 | 35 | ### 5. Build Distribution Files 36 | ```bash 37 | npm run build 38 | ``` 39 | 40 | ### 6. Commit Changes 41 | ```bash 42 | git add -A 43 | git commit -m "Release v{version}" 44 | ``` 45 | **IMPORTANT:** Do NOT add co-author attribution. Claude Code should never add itself as a co-author unless specifically requested by the user. 46 | 47 | ### 7. Create Git Tag 48 | ```bash 49 | git tag v{version} 50 | git push origin main --tags 51 | ``` 52 | 53 | ### 8. Publish to NPM 54 | ```bash 55 | npm publish 56 | ``` 57 | 58 | ### 9. Create GitHub Release 59 | - Go to GitHub releases page 60 | - Create release from tag 61 | - Copy CHANGELOG entry as release notes 62 | - Attach dist files if needed 63 | 64 | ### 10. Post-Release 65 | - Verify npm package is live 66 | - Test installation in a clean project 67 | - Update any example repos or documentation sites -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OverType - Interactive Demo 7 | 149 | 150 | 151 | View on GitHub → 152 | 153 |
154 | OverType 155 |

Interactive Demo - Try all the features

156 |
157 | 158 |
159 | 160 |
161 |
162 |

Main Editor

163 |
164 | 165 | 166 |
167 |
168 |
169 |
170 | 171 | 172 |
173 |
174 |

Live Preview

175 |
176 | 177 |
178 |
179 |
180 |
181 | 182 | 183 |
184 |
185 |

Notes

186 |
187 | 188 |
189 |
190 |
191 |
192 | 193 | 194 |
195 |
196 |

Code Editor

197 |
198 |
199 |
200 |
201 | 202 | 203 |
204 | 207 |
208 | 209 | 210 | 328 | 329 | -------------------------------------------------------------------------------- /diagram-embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 221 | 222 | 223 |
224 | 225 |
226 | 227 | 228 |
229 | 230 |
231 |
Invisible Textarea
232 |
233 | 234 |
235 |
236 |
237 | 238 | 239 |
240 |
Rendered Markdown
241 |
242 |
243 |
244 |
245 |
246 | 247 | 421 | 422 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/diagram.png -------------------------------------------------------------------------------- /dist/overtype.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for OverType 2 | // Project: https://github.com/panphora/overtype 3 | // Definitions generated from JSDoc comments and implementation 4 | 5 | export interface Theme { 6 | name: string; 7 | colors: { 8 | bgPrimary?: string; 9 | bgSecondary?: string; 10 | text?: string; 11 | textSecondary?: string; 12 | h1?: string; 13 | h2?: string; 14 | h3?: string; 15 | strong?: string; 16 | em?: string; 17 | link?: string; 18 | code?: string; 19 | codeBg?: string; 20 | blockquote?: string; 21 | hr?: string; 22 | syntaxMarker?: string; 23 | listMarker?: string; 24 | cursor?: string; 25 | selection?: string; 26 | rawLine?: string; 27 | // Toolbar theme colors 28 | toolbarBg?: string; 29 | toolbarIcon?: string; 30 | toolbarHover?: string; 31 | toolbarActive?: string; 32 | border?: string; 33 | }; 34 | } 35 | 36 | export interface Stats { 37 | words: number; 38 | chars: number; 39 | lines: number; 40 | line: number; 41 | column: number; 42 | } 43 | 44 | export interface MobileOptions { 45 | fontSize?: string; 46 | padding?: string; 47 | lineHeight?: string | number; 48 | } 49 | 50 | export interface Options { 51 | // Typography 52 | fontSize?: string; 53 | lineHeight?: string | number; 54 | fontFamily?: string; 55 | padding?: string; 56 | 57 | // Mobile responsive 58 | mobile?: MobileOptions; 59 | 60 | // Native textarea attributes (v1.1.2+) 61 | textareaProps?: Record; 62 | 63 | // Behavior 64 | autofocus?: boolean; 65 | autoResize?: boolean; // v1.1.2+ Auto-expand height with content 66 | minHeight?: string; // v1.1.2+ Minimum height for autoResize mode 67 | maxHeight?: string | null; // v1.1.2+ Maximum height for autoResize mode 68 | placeholder?: string; 69 | value?: string; 70 | 71 | // Features 72 | showActiveLineRaw?: boolean; 73 | showStats?: boolean; 74 | toolbar?: boolean | { 75 | buttons?: Array<{ 76 | name?: string; 77 | icon?: string; 78 | title?: string; 79 | action?: string; 80 | separator?: boolean; 81 | }>; 82 | }; 83 | smartLists?: boolean; // v1.2.3+ Smart list continuation 84 | statsFormatter?: (stats: Stats) => string; 85 | 86 | // Theme (deprecated in favor of global theme) 87 | theme?: string | Theme; 88 | colors?: Partial; 89 | 90 | // Callbacks 91 | onChange?: (value: string, instance: OverTypeInstance) => void; 92 | onKeydown?: (event: KeyboardEvent, instance: OverTypeInstance) => void; 93 | } 94 | 95 | // Interface for constructor that returns array 96 | export interface OverTypeConstructor { 97 | new(target: string | Element | NodeList | Element[], options?: Options): OverTypeInstance[]; 98 | // Static members 99 | instances: WeakMap; 100 | stylesInjected: boolean; 101 | globalListenersInitialized: boolean; 102 | instanceCount: number; 103 | currentTheme: Theme; 104 | themes: { 105 | solar: Theme; 106 | cave: Theme; 107 | }; 108 | MarkdownParser: any; 109 | ShortcutsManager: any; 110 | init(target: string | Element | NodeList | Element[], options?: Options): OverTypeInstance[]; 111 | getInstance(element: Element): OverTypeInstance | null; 112 | destroyAll(): void; 113 | injectStyles(force?: boolean): void; 114 | setTheme(theme: string | Theme, customColors?: Partial): void; 115 | initGlobalListeners(): void; 116 | getTheme(name: string): Theme; 117 | } 118 | 119 | export interface RenderOptions { 120 | cleanHTML?: boolean; 121 | } 122 | 123 | export interface OverTypeInstance { 124 | // Public properties 125 | container: HTMLElement; 126 | wrapper: HTMLElement; 127 | textarea: HTMLTextAreaElement; 128 | preview: HTMLElement; 129 | statsBar?: HTMLElement; 130 | toolbar?: any; // Toolbar instance 131 | shortcuts?: any; // ShortcutsManager instance 132 | linkTooltip?: any; // LinkTooltip instance 133 | options: Options; 134 | initialized: boolean; 135 | instanceId: number; 136 | element: Element; 137 | 138 | // Public methods 139 | getValue(): string; 140 | setValue(value: string): void; 141 | getStats(): Stats; 142 | getContainer(): HTMLElement; 143 | focus(): void; 144 | blur(): void; 145 | destroy(): void; 146 | isInitialized(): boolean; 147 | reinit(options: Options): void; 148 | showStats(show: boolean): void; 149 | setTheme(theme: string | Theme): void; 150 | updatePreview(): void; 151 | 152 | // HTML output methods 153 | getRenderedHTML(options?: RenderOptions): string; 154 | getCleanHTML(): string; 155 | getPreviewHTML(): string; 156 | 157 | // View mode methods 158 | showPlainTextarea(show: boolean): void; 159 | showPreviewMode(show: boolean): void; 160 | } 161 | 162 | // Declare the constructor as a constant with proper typing 163 | declare const OverType: OverTypeConstructor; 164 | 165 | // Export the instance type under a different name for clarity 166 | export type OverType = OverTypeInstance; 167 | 168 | // Module exports - default export is the constructor 169 | export default OverType; -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OverType - Basic Example 7 | 67 | 68 | 69 |

OverType Basic Example

70 |

A simple markdown editor with live preview

71 | 72 |
73 | 74 |
75 | 76 | 77 | 78 | 79 |
80 | 81 | 82 | 83 | 84 | 182 | 183 | -------------------------------------------------------------------------------- /examples/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OverType - Multiple Editors Example 7 | 96 | 97 | 98 |

OverType Multiple Editors Example

99 |

Multiple independent editors on the same page

100 | 101 |
102 |
103 |
104 | Editor 1 - Notes 105 | 106 |
107 |
108 |
109 | 110 |
111 |
112 | Editor 2 - Todo List 113 | 114 |
115 |
116 |
117 | 118 |
119 |
120 | Editor 3 - Code Snippets 121 | 122 |
123 |
124 |
125 | 126 |
127 |
128 | Editor 4 - Documentation 129 | 130 |
131 |
132 |
133 |
134 | 135 |
136 | 137 | 138 | 139 | 140 | 141 |
142 | 143 | 144 | 145 | 146 | 318 | 319 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/favicon.ico -------------------------------------------------------------------------------- /open-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panphora/overtype/a973ddd621e92461774998ca729da236ea1c8718/open-graph.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overtype", 3 | "version": "1.2.7", 4 | "description": "A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay", 5 | "main": "dist/overtype.cjs", 6 | "module": "dist/overtype.esm.js", 7 | "browser": "dist/overtype.min.js", 8 | "types": "dist/overtype.d.ts", 9 | "unpkg": "dist/overtype.min.js", 10 | "jsdelivr": "dist/overtype.min.js", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/overtype.d.ts", 14 | "import": "./dist/overtype.esm.js", 15 | "require": "./dist/overtype.cjs", 16 | "browser": "./dist/overtype.iife.min.js" 17 | } 18 | }, 19 | "type": "module", 20 | "scripts": { 21 | "build": "node build.js", 22 | "build:prod": "npm test && npm run build", 23 | "dev": "http-server -p 8080 -c-1", 24 | "watch": "node build.js --watch", 25 | "test": "node test/overtype.test.js && node test/preview-mode.test.js && node test/links.test.js && node test/api-methods.test.js && node test/comprehensive-alignment.test.js && node test/sanctuary-parsing.test.js && node test/mode-switching.test.js && npm run test:types", 26 | "test:main": "node test/overtype.test.js", 27 | "test:preview": "node test/preview-mode.test.js", 28 | "test:links": "node test/links.test.js", 29 | "test:api": "node test/api-methods.test.js", 30 | "test:alignment": "node test/comprehensive-alignment.test.js", 31 | "test:sanctuary": "node test/sanctuary-parsing.test.js", 32 | "test:modes": "node test/mode-switching.test.js", 33 | "test:types": "tsc --noEmit test-types.ts", 34 | "preversion": "npm test", 35 | "size": "gzip-size dist/overtype.min.js", 36 | "serve": "http-server -p 8080 -c-1" 37 | }, 38 | "keywords": [ 39 | "markdown", 40 | "editor", 41 | "wysiwyg", 42 | "lightweight", 43 | "ghost-caret", 44 | "textarea", 45 | "markdown-editor" 46 | ], 47 | "author": "David Miranda", 48 | "license": "MIT", 49 | "devDependencies": { 50 | "esbuild": "^0.19.0", 51 | "gzip-size-cli": "^5.1.0", 52 | "http-server": "^14.1.1", 53 | "jsdom": "^26.1.0", 54 | "typescript": "^5.9.2" 55 | }, 56 | "files": [ 57 | "dist", 58 | "src", 59 | "README.md", 60 | "LICENSE", 61 | "diagram.png" 62 | ], 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/panphora/overtype.git" 66 | }, 67 | "bugs": { 68 | "url": "https://github.com/panphora/overtype/issues" 69 | }, 70 | "homepage": "https://github.com/panphora/overtype#readme", 71 | "dependencies": { 72 | "markdown-actions": "^1.1.2" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/icons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SVG icons for OverType toolbar 3 | * Quill-style icons with inline styles 4 | */ 5 | 6 | export const boldIcon = ` 7 | 8 | 9 | `; 10 | 11 | export const italicIcon = ` 12 | 13 | 14 | 15 | `; 16 | 17 | export const h1Icon = ` 18 | 19 | `; 20 | 21 | export const h2Icon = ` 22 | 23 | `; 24 | 25 | export const h3Icon = ` 26 | 27 | `; 28 | 29 | export const linkIcon = ` 30 | 31 | 32 | 33 | `; 34 | 35 | export const codeIcon = ` 36 | 37 | 38 | 39 | `; 40 | 41 | 42 | export const bulletListIcon = ` 43 | 44 | 45 | 46 | 47 | 48 | 49 | `; 50 | 51 | export const orderedListIcon = ` 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | `; 60 | 61 | export const quoteIcon = ` 62 | 63 | 64 | `; 65 | 66 | export const taskListIcon = ` 67 | 68 | 69 | 70 | 71 | 72 | 73 | `; 74 | 75 | export const eyeIcon = ` 76 | 77 | 78 | `; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Entry point for browser builds 2 | import OverType from './overtype.js'; 3 | 4 | export default OverType; -------------------------------------------------------------------------------- /src/link-tooltip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Link Tooltip - CSS Anchor Positioning with index-based anchors 3 | * Shows a clickable tooltip when cursor is within a link 4 | * Uses CSS anchor positioning with dynamically selected anchor 5 | */ 6 | 7 | export class LinkTooltip { 8 | constructor(editor) { 9 | this.editor = editor; 10 | this.tooltip = null; 11 | this.currentLink = null; 12 | this.hideTimeout = null; 13 | 14 | this.init(); 15 | } 16 | 17 | init() { 18 | // Check for CSS anchor positioning support 19 | const supportsAnchor = 20 | CSS.supports('position-anchor: --x') && 21 | CSS.supports('position-area: center'); 22 | 23 | if (!supportsAnchor) { 24 | // Don't show anything if not supported 25 | return; 26 | } 27 | 28 | // Create tooltip element 29 | this.createTooltip(); 30 | 31 | // Listen for cursor position changes 32 | this.editor.textarea.addEventListener('selectionchange', () => this.checkCursorPosition()); 33 | this.editor.textarea.addEventListener('keyup', (e) => { 34 | if (e.key.includes('Arrow') || e.key === 'Home' || e.key === 'End') { 35 | this.checkCursorPosition(); 36 | } 37 | }); 38 | 39 | // Hide tooltip when typing or scrolling 40 | this.editor.textarea.addEventListener('input', () => this.hide()); 41 | this.editor.textarea.addEventListener('scroll', () => this.hide()); 42 | 43 | // Keep tooltip visible on hover 44 | this.tooltip.addEventListener('mouseenter', () => this.cancelHide()); 45 | this.tooltip.addEventListener('mouseleave', () => this.scheduleHide()); 46 | } 47 | 48 | createTooltip() { 49 | // Create tooltip element 50 | this.tooltip = document.createElement('div'); 51 | this.tooltip.className = 'overtype-link-tooltip'; 52 | 53 | // Add CSS anchor positioning styles 54 | const tooltipStyles = document.createElement('style'); 55 | tooltipStyles.textContent = ` 56 | @supports (position-anchor: --x) and (position-area: center) { 57 | .overtype-link-tooltip { 58 | position: absolute; 59 | position-anchor: var(--target-anchor, --link-0); 60 | position-area: block-end center; 61 | margin-top: 8px !important; 62 | 63 | background: #333 !important; 64 | color: white !important; 65 | padding: 6px 10px !important; 66 | border-radius: 16px !important; 67 | font-size: 12px !important; 68 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; 69 | display: none !important; 70 | z-index: 10000 !important; 71 | cursor: pointer !important; 72 | box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; 73 | max-width: 300px !important; 74 | white-space: nowrap !important; 75 | overflow: hidden !important; 76 | text-overflow: ellipsis !important; 77 | 78 | position-try: most-width block-end inline-end, flip-inline, block-start center; 79 | position-visibility: anchors-visible; 80 | } 81 | 82 | .overtype-link-tooltip.visible { 83 | display: flex !important; 84 | } 85 | } 86 | `; 87 | document.head.appendChild(tooltipStyles); 88 | 89 | // Add link icon and text container 90 | this.tooltip.innerHTML = ` 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | `; 99 | 100 | // Click handler to open link 101 | this.tooltip.addEventListener('click', (e) => { 102 | e.preventDefault(); 103 | e.stopPropagation(); 104 | if (this.currentLink) { 105 | window.open(this.currentLink.url, '_blank'); 106 | this.hide(); 107 | } 108 | }); 109 | 110 | // Append tooltip to editor container 111 | this.editor.container.appendChild(this.tooltip); 112 | } 113 | 114 | checkCursorPosition() { 115 | const cursorPos = this.editor.textarea.selectionStart; 116 | const text = this.editor.textarea.value; 117 | 118 | // Find if cursor is within a markdown link 119 | const linkInfo = this.findLinkAtPosition(text, cursorPos); 120 | 121 | if (linkInfo) { 122 | if (!this.currentLink || this.currentLink.url !== linkInfo.url || this.currentLink.index !== linkInfo.index) { 123 | this.show(linkInfo); 124 | } 125 | } else { 126 | this.scheduleHide(); 127 | } 128 | } 129 | 130 | findLinkAtPosition(text, position) { 131 | // Regex to find markdown links: [text](url) 132 | const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; 133 | let match; 134 | let linkIndex = 0; 135 | 136 | while ((match = linkRegex.exec(text)) !== null) { 137 | const start = match.index; 138 | const end = match.index + match[0].length; 139 | 140 | if (position >= start && position <= end) { 141 | return { 142 | text: match[1], 143 | url: match[2], 144 | index: linkIndex, 145 | start: start, 146 | end: end 147 | }; 148 | } 149 | linkIndex++; 150 | } 151 | 152 | return null; 153 | } 154 | 155 | show(linkInfo) { 156 | this.currentLink = linkInfo; 157 | this.cancelHide(); 158 | 159 | // Update tooltip content 160 | const urlSpan = this.tooltip.querySelector('.overtype-link-tooltip-url'); 161 | urlSpan.textContent = linkInfo.url; 162 | 163 | // Set the CSS variable to point to the correct anchor 164 | this.tooltip.style.setProperty('--target-anchor', `--link-${linkInfo.index}`); 165 | 166 | // Show tooltip (CSS anchor positioning handles the rest) 167 | this.tooltip.classList.add('visible'); 168 | } 169 | 170 | hide() { 171 | this.tooltip.classList.remove('visible'); 172 | this.currentLink = null; 173 | } 174 | 175 | scheduleHide() { 176 | this.cancelHide(); 177 | this.hideTimeout = setTimeout(() => this.hide(), 300); 178 | } 179 | 180 | cancelHide() { 181 | if (this.hideTimeout) { 182 | clearTimeout(this.hideTimeout); 183 | this.hideTimeout = null; 184 | } 185 | } 186 | 187 | destroy() { 188 | this.cancelHide(); 189 | if (this.tooltip && this.tooltip.parentNode) { 190 | this.tooltip.parentNode.removeChild(this.tooltip); 191 | } 192 | this.tooltip = null; 193 | this.currentLink = null; 194 | } 195 | } -------------------------------------------------------------------------------- /src/overtype.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for OverType 2 | // Project: https://github.com/panphora/overtype 3 | // Definitions generated from JSDoc comments and implementation 4 | 5 | export interface Theme { 6 | name: string; 7 | colors: { 8 | bgPrimary?: string; 9 | bgSecondary?: string; 10 | text?: string; 11 | textSecondary?: string; 12 | h1?: string; 13 | h2?: string; 14 | h3?: string; 15 | strong?: string; 16 | em?: string; 17 | link?: string; 18 | code?: string; 19 | codeBg?: string; 20 | blockquote?: string; 21 | hr?: string; 22 | syntaxMarker?: string; 23 | listMarker?: string; 24 | cursor?: string; 25 | selection?: string; 26 | rawLine?: string; 27 | // Toolbar theme colors 28 | toolbarBg?: string; 29 | toolbarIcon?: string; 30 | toolbarHover?: string; 31 | toolbarActive?: string; 32 | border?: string; 33 | }; 34 | } 35 | 36 | export interface Stats { 37 | words: number; 38 | chars: number; 39 | lines: number; 40 | line: number; 41 | column: number; 42 | } 43 | 44 | export interface MobileOptions { 45 | fontSize?: string; 46 | padding?: string; 47 | lineHeight?: string | number; 48 | } 49 | 50 | export interface Options { 51 | // Typography 52 | fontSize?: string; 53 | lineHeight?: string | number; 54 | fontFamily?: string; 55 | padding?: string; 56 | 57 | // Mobile responsive 58 | mobile?: MobileOptions; 59 | 60 | // Native textarea attributes (v1.1.2+) 61 | textareaProps?: Record; 62 | 63 | // Behavior 64 | autofocus?: boolean; 65 | autoResize?: boolean; // v1.1.2+ Auto-expand height with content 66 | minHeight?: string; // v1.1.2+ Minimum height for autoResize mode 67 | maxHeight?: string | null; // v1.1.2+ Maximum height for autoResize mode 68 | placeholder?: string; 69 | value?: string; 70 | 71 | // Features 72 | showActiveLineRaw?: boolean; 73 | showStats?: boolean; 74 | toolbar?: boolean | { 75 | buttons?: Array<{ 76 | name?: string; 77 | icon?: string; 78 | title?: string; 79 | action?: string; 80 | separator?: boolean; 81 | }>; 82 | }; 83 | smartLists?: boolean; // v1.2.3+ Smart list continuation 84 | statsFormatter?: (stats: Stats) => string; 85 | 86 | // Theme (deprecated in favor of global theme) 87 | theme?: string | Theme; 88 | colors?: Partial; 89 | 90 | // Callbacks 91 | onChange?: (value: string, instance: OverTypeInstance) => void; 92 | onKeydown?: (event: KeyboardEvent, instance: OverTypeInstance) => void; 93 | } 94 | 95 | // Interface for constructor that returns array 96 | export interface OverTypeConstructor { 97 | new(target: string | Element | NodeList | Element[], options?: Options): OverTypeInstance[]; 98 | // Static members 99 | instances: WeakMap; 100 | stylesInjected: boolean; 101 | globalListenersInitialized: boolean; 102 | instanceCount: number; 103 | currentTheme: Theme; 104 | themes: { 105 | solar: Theme; 106 | cave: Theme; 107 | }; 108 | MarkdownParser: any; 109 | ShortcutsManager: any; 110 | init(target: string | Element | NodeList | Element[], options?: Options): OverTypeInstance[]; 111 | getInstance(element: Element): OverTypeInstance | null; 112 | destroyAll(): void; 113 | injectStyles(force?: boolean): void; 114 | setTheme(theme: string | Theme, customColors?: Partial): void; 115 | initGlobalListeners(): void; 116 | getTheme(name: string): Theme; 117 | } 118 | 119 | export interface RenderOptions { 120 | cleanHTML?: boolean; 121 | } 122 | 123 | export interface OverTypeInstance { 124 | // Public properties 125 | container: HTMLElement; 126 | wrapper: HTMLElement; 127 | textarea: HTMLTextAreaElement; 128 | preview: HTMLElement; 129 | statsBar?: HTMLElement; 130 | toolbar?: any; // Toolbar instance 131 | shortcuts?: any; // ShortcutsManager instance 132 | linkTooltip?: any; // LinkTooltip instance 133 | options: Options; 134 | initialized: boolean; 135 | instanceId: number; 136 | element: Element; 137 | 138 | // Public methods 139 | getValue(): string; 140 | setValue(value: string): void; 141 | getStats(): Stats; 142 | getContainer(): HTMLElement; 143 | focus(): void; 144 | blur(): void; 145 | destroy(): void; 146 | isInitialized(): boolean; 147 | reinit(options: Options): void; 148 | showStats(show: boolean): void; 149 | setTheme(theme: string | Theme): void; 150 | updatePreview(): void; 151 | 152 | // HTML output methods 153 | getRenderedHTML(options?: RenderOptions): string; 154 | getCleanHTML(): string; 155 | getPreviewHTML(): string; 156 | 157 | // View mode methods 158 | showPlainTextarea(show: boolean): void; 159 | showPreviewMode(show: boolean): void; 160 | } 161 | 162 | // Declare the constructor as a constant with proper typing 163 | declare const OverType: OverTypeConstructor; 164 | 165 | // Export the instance type under a different name for clarity 166 | export type OverType = OverTypeInstance; 167 | 168 | // Module exports - default export is the constructor 169 | export default OverType; -------------------------------------------------------------------------------- /src/shortcuts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Keyboard shortcuts handler for OverType editor 3 | * Uses the same handleAction method as toolbar for consistency 4 | */ 5 | 6 | import * as markdownActions from 'markdown-actions'; 7 | 8 | /** 9 | * ShortcutsManager - Handles keyboard shortcuts for the editor 10 | */ 11 | export class ShortcutsManager { 12 | constructor(editor) { 13 | this.editor = editor; 14 | this.textarea = editor.textarea; 15 | // No need to add our own listener - OverType will call handleKeydown 16 | } 17 | 18 | /** 19 | * Handle keydown events - called by OverType 20 | * @param {KeyboardEvent} event - The keyboard event 21 | * @returns {boolean} Whether the event was handled 22 | */ 23 | handleKeydown(event) { 24 | const isMac = navigator.platform.toLowerCase().includes('mac'); 25 | const modKey = isMac ? event.metaKey : event.ctrlKey; 26 | 27 | if (!modKey) return false; 28 | 29 | let action = null; 30 | 31 | // Map keyboard shortcuts to toolbar actions 32 | switch(event.key.toLowerCase()) { 33 | case 'b': 34 | if (!event.shiftKey) { 35 | action = 'toggleBold'; 36 | } 37 | break; 38 | 39 | case 'i': 40 | if (!event.shiftKey) { 41 | action = 'toggleItalic'; 42 | } 43 | break; 44 | 45 | case 'k': 46 | if (!event.shiftKey) { 47 | action = 'insertLink'; 48 | } 49 | break; 50 | 51 | case '7': 52 | if (event.shiftKey) { 53 | action = 'toggleNumberedList'; 54 | } 55 | break; 56 | 57 | case '8': 58 | if (event.shiftKey) { 59 | action = 'toggleBulletList'; 60 | } 61 | break; 62 | } 63 | 64 | // If we have an action, handle it exactly like the toolbar does 65 | if (action) { 66 | event.preventDefault(); 67 | 68 | // If toolbar exists, use its handleAction method (exact same code path) 69 | if (this.editor.toolbar) { 70 | this.editor.toolbar.handleAction(action); 71 | } else { 72 | // Fallback: duplicate the toolbar's handleAction logic 73 | this.handleAction(action); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | return false; 80 | } 81 | 82 | /** 83 | * Handle action - fallback when no toolbar exists 84 | * This duplicates toolbar.handleAction for consistency 85 | */ 86 | async handleAction(action) { 87 | const textarea = this.textarea; 88 | if (!textarea) return; 89 | 90 | // Focus textarea 91 | textarea.focus(); 92 | 93 | try { 94 | switch (action) { 95 | case 'toggleBold': 96 | markdownActions.toggleBold(textarea); 97 | break; 98 | case 'toggleItalic': 99 | markdownActions.toggleItalic(textarea); 100 | break; 101 | case 'insertLink': 102 | markdownActions.insertLink(textarea); 103 | break; 104 | case 'toggleBulletList': 105 | markdownActions.toggleBulletList(textarea); 106 | break; 107 | case 'toggleNumberedList': 108 | markdownActions.toggleNumberedList(textarea); 109 | break; 110 | } 111 | 112 | // Trigger input event to update preview 113 | textarea.dispatchEvent(new Event('input', { bubbles: true })); 114 | } catch (error) { 115 | console.error('Error in markdown action:', error); 116 | } 117 | } 118 | 119 | /** 120 | * Cleanup 121 | */ 122 | destroy() { 123 | // Nothing to clean up since we don't add our own listener 124 | } 125 | } -------------------------------------------------------------------------------- /src/themes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in themes for OverType editor 3 | * Each theme provides a complete color palette for the editor 4 | */ 5 | 6 | /** 7 | * Solar theme - Light, warm and bright 8 | */ 9 | export const solar = { 10 | name: 'solar', 11 | colors: { 12 | bgPrimary: '#faf0ca', // Lemon Chiffon - main background 13 | bgSecondary: '#ffffff', // White - editor background 14 | text: '#0d3b66', // Yale Blue - main text 15 | h1: '#f95738', // Tomato - h1 headers 16 | h2: '#ee964b', // Sandy Brown - h2 headers 17 | h3: '#3d8a51', // Forest green - h3 headers 18 | strong: '#ee964b', // Sandy Brown - bold text 19 | em: '#f95738', // Tomato - italic text 20 | link: '#0d3b66', // Yale Blue - links 21 | code: '#0d3b66', // Yale Blue - inline code 22 | codeBg: 'rgba(244, 211, 94, 0.4)', // Naples Yellow with transparency 23 | blockquote: '#5a7a9b', // Muted blue - blockquotes 24 | hr: '#5a7a9b', // Muted blue - horizontal rules 25 | syntaxMarker: 'rgba(13, 59, 102, 0.52)', // Yale Blue with transparency 26 | cursor: '#f95738', // Tomato - cursor 27 | selection: 'rgba(244, 211, 94, 0.4)', // Naples Yellow with transparency 28 | listMarker: '#ee964b', // Sandy Brown - list markers 29 | // Toolbar colors 30 | toolbarBg: '#ffffff', // White - toolbar background 31 | toolbarBorder: 'rgba(13, 59, 102, 0.15)', // Yale Blue border 32 | toolbarIcon: '#0d3b66', // Yale Blue - icon color 33 | toolbarHover: '#f5f5f5', // Light gray - hover background 34 | toolbarActive: '#faf0ca', // Lemon Chiffon - active button background 35 | } 36 | }; 37 | 38 | /** 39 | * Cave theme - Dark ocean depths 40 | */ 41 | export const cave = { 42 | name: 'cave', 43 | colors: { 44 | bgPrimary: '#141E26', // Deep ocean - main background 45 | bgSecondary: '#1D2D3E', // Darker charcoal - editor background 46 | text: '#c5dde8', // Light blue-gray - main text 47 | h1: '#d4a5ff', // Rich lavender - h1 headers 48 | h2: '#f6ae2d', // Hunyadi Yellow - h2 headers 49 | h3: '#9fcfec', // Brighter blue - h3 headers 50 | strong: '#f6ae2d', // Hunyadi Yellow - bold text 51 | em: '#9fcfec', // Brighter blue - italic text 52 | link: '#9fcfec', // Brighter blue - links 53 | code: '#c5dde8', // Light blue-gray - inline code 54 | codeBg: '#1a232b', // Very dark blue - code background 55 | blockquote: '#9fcfec', // Brighter blue - same as italic 56 | hr: '#c5dde8', // Light blue-gray - horizontal rules 57 | syntaxMarker: 'rgba(159, 207, 236, 0.73)', // Brighter blue semi-transparent 58 | cursor: '#f26419', // Orange Pantone - cursor 59 | selection: 'rgba(51, 101, 138, 0.4)', // Lapis Lazuli with transparency 60 | listMarker: '#f6ae2d', // Hunyadi Yellow - list markers 61 | // Toolbar colors for dark theme 62 | toolbarBg: '#1D2D3E', // Darker charcoal - toolbar background 63 | toolbarBorder: 'rgba(197, 221, 232, 0.1)', // Light blue-gray border 64 | toolbarIcon: '#c5dde8', // Light blue-gray - icon color 65 | toolbarHover: '#243546', // Slightly lighter charcoal - hover background 66 | toolbarActive: '#2a3f52', // Even lighter - active button background 67 | } 68 | }; 69 | 70 | /** 71 | * Default themes registry 72 | */ 73 | export const themes = { 74 | solar, 75 | cave, 76 | // Aliases for backward compatibility 77 | light: solar, 78 | dark: cave 79 | }; 80 | 81 | /** 82 | * Get theme by name or return custom theme object 83 | * @param {string|Object} theme - Theme name or custom theme object 84 | * @returns {Object} Theme configuration 85 | */ 86 | export function getTheme(theme) { 87 | if (typeof theme === 'string') { 88 | const themeObj = themes[theme] || themes.solar; 89 | // Preserve the requested theme name (important for 'light' and 'dark' aliases) 90 | return { ...themeObj, name: theme }; 91 | } 92 | return theme; 93 | } 94 | 95 | /** 96 | * Apply theme colors to CSS variables 97 | * @param {Object} colors - Theme colors object 98 | * @returns {string} CSS custom properties string 99 | */ 100 | export function themeToCSSVars(colors) { 101 | const vars = []; 102 | for (const [key, value] of Object.entries(colors)) { 103 | // Convert camelCase to kebab-case 104 | const varName = key.replace(/([A-Z])/g, '-$1').toLowerCase(); 105 | vars.push(`--${varName}: ${value};`); 106 | } 107 | return vars.join('\n'); 108 | } 109 | 110 | /** 111 | * Merge custom colors with base theme 112 | * @param {Object} baseTheme - Base theme object 113 | * @param {Object} customColors - Custom color overrides 114 | * @returns {Object} Merged theme object 115 | */ 116 | export function mergeTheme(baseTheme, customColors = {}) { 117 | return { 118 | ...baseTheme, 119 | colors: { 120 | ...baseTheme.colors, 121 | ...customColors 122 | } 123 | }; 124 | } -------------------------------------------------------------------------------- /src/toolbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Toolbar component for OverType editor 3 | * Provides markdown formatting buttons with icons 4 | */ 5 | 6 | import * as icons from './icons.js'; 7 | import * as markdownActions from 'markdown-actions'; 8 | 9 | export class Toolbar { 10 | constructor(editor, buttonConfig = null) { 11 | this.editor = editor; 12 | this.container = null; 13 | this.buttons = {}; 14 | this.buttonConfig = buttonConfig; 15 | } 16 | 17 | 18 | /** 19 | * Create and attach toolbar to editor 20 | */ 21 | create() { 22 | // Create toolbar container 23 | this.container = document.createElement('div'); 24 | this.container.className = 'overtype-toolbar'; 25 | this.container.setAttribute('role', 'toolbar'); 26 | this.container.setAttribute('aria-label', 'Text formatting'); 27 | 28 | // Define toolbar buttons 29 | const buttonConfig = this.buttonConfig ?? [ 30 | { name: 'bold', icon: icons.boldIcon, title: 'Bold (Ctrl+B)', action: 'toggleBold' }, 31 | { name: 'italic', icon: icons.italicIcon, title: 'Italic (Ctrl+I)', action: 'toggleItalic' }, 32 | { separator: true }, 33 | { name: 'h1', icon: icons.h1Icon, title: 'Heading 1', action: 'insertH1' }, 34 | { name: 'h2', icon: icons.h2Icon, title: 'Heading 2', action: 'insertH2' }, 35 | { name: 'h3', icon: icons.h3Icon, title: 'Heading 3', action: 'insertH3' }, 36 | { separator: true }, 37 | { name: 'link', icon: icons.linkIcon, title: 'Insert Link (Ctrl+K)', action: 'insertLink' }, 38 | { name: 'code', icon: icons.codeIcon, title: 'Code (Ctrl+`)', action: 'toggleCode' }, 39 | { separator: true }, 40 | { name: 'quote', icon: icons.quoteIcon, title: 'Quote', action: 'toggleQuote' }, 41 | { separator: true }, 42 | { name: 'bulletList', icon: icons.bulletListIcon, title: 'Bullet List', action: 'toggleBulletList' }, 43 | { name: 'orderedList', icon: icons.orderedListIcon, title: 'Numbered List', action: 'toggleNumberedList' }, 44 | { name: 'taskList', icon: icons.taskListIcon, title: 'Task List', action: 'toggleTaskList' }, 45 | { separator: true }, 46 | { name: 'viewMode', icon: icons.eyeIcon, title: 'View mode', action: 'toggle-view-menu', hasDropdown: true } 47 | ]; 48 | 49 | // Create buttons 50 | buttonConfig.forEach(config => { 51 | if (config.separator) { 52 | const separator = document.createElement('div'); 53 | separator.className = 'overtype-toolbar-separator'; 54 | separator.setAttribute('role', 'separator'); 55 | this.container.appendChild(separator); 56 | } else { 57 | const button = this.createButton(config); 58 | this.buttons[config.name] = button; 59 | this.container.appendChild(button); 60 | } 61 | }); 62 | 63 | // Insert toolbar into container before editor wrapper 64 | const container = this.editor.element.querySelector('.overtype-container'); 65 | const wrapper = this.editor.element.querySelector('.overtype-wrapper'); 66 | if (container && wrapper) { 67 | container.insertBefore(this.container, wrapper); 68 | } 69 | 70 | return this.container; 71 | } 72 | 73 | /** 74 | * Create individual toolbar button 75 | */ 76 | createButton(config) { 77 | const button = document.createElement('button'); 78 | button.className = 'overtype-toolbar-button'; 79 | button.type = 'button'; 80 | button.title = config.title; 81 | button.setAttribute('aria-label', config.title); 82 | button.setAttribute('data-action', config.action); 83 | button.innerHTML = config.icon; 84 | 85 | // Add dropdown if needed 86 | if (config.hasDropdown) { 87 | button.classList.add('has-dropdown'); 88 | // Store reference for dropdown 89 | if (config.name === 'viewMode') { 90 | this.viewModeButton = button; 91 | } 92 | } 93 | 94 | // Add click handler 95 | button.addEventListener('click', (e) => { 96 | e.preventDefault(); 97 | this.handleAction(config.action, button); 98 | }); 99 | 100 | return button; 101 | } 102 | 103 | /** 104 | * Handle toolbar button actions 105 | */ 106 | async handleAction(action, button) { 107 | const textarea = this.editor.textarea; 108 | if (!textarea) return; 109 | 110 | // Handle dropdown toggle 111 | if (action === 'toggle-view-menu') { 112 | this.toggleViewDropdown(button); 113 | return; 114 | } 115 | 116 | // Focus textarea for other actions 117 | textarea.focus(); 118 | 119 | try { 120 | 121 | switch (action) { 122 | case 'toggleBold': 123 | markdownActions.toggleBold(textarea); 124 | break; 125 | case 'toggleItalic': 126 | markdownActions.toggleItalic(textarea); 127 | break; 128 | case 'insertH1': 129 | markdownActions.toggleH1(textarea); 130 | break; 131 | case 'insertH2': 132 | markdownActions.toggleH2(textarea); 133 | break; 134 | case 'insertH3': 135 | markdownActions.toggleH3(textarea); 136 | break; 137 | case 'insertLink': 138 | markdownActions.insertLink(textarea); 139 | break; 140 | case 'toggleCode': 141 | markdownActions.toggleCode(textarea); 142 | break; 143 | case 'toggleBulletList': 144 | markdownActions.toggleBulletList(textarea); 145 | break; 146 | case 'toggleNumberedList': 147 | markdownActions.toggleNumberedList(textarea); 148 | break; 149 | case 'toggleQuote': 150 | markdownActions.toggleQuote(textarea); 151 | break; 152 | case 'toggleTaskList': 153 | markdownActions.toggleTaskList(textarea); 154 | break; 155 | case 'toggle-plain': 156 | // Toggle between plain textarea and overlay mode 157 | const isPlain = this.editor.container.classList.contains('plain-mode'); 158 | this.editor.showPlainTextarea(!isPlain); 159 | break; 160 | } 161 | 162 | // Trigger input event to update preview 163 | textarea.dispatchEvent(new Event('input', { bubbles: true })); 164 | } catch (error) { 165 | console.error('Error loading markdown-actions:', error); 166 | } 167 | } 168 | 169 | /** 170 | * Update toolbar button states based on current selection 171 | */ 172 | async updateButtonStates() { 173 | const textarea = this.editor.textarea; 174 | if (!textarea) return; 175 | 176 | try { 177 | const activeFormats = markdownActions.getActiveFormats(textarea); 178 | 179 | // Update button states 180 | Object.entries(this.buttons).forEach(([name, button]) => { 181 | let isActive = false; 182 | 183 | switch (name) { 184 | case 'bold': 185 | isActive = activeFormats.includes('bold'); 186 | break; 187 | case 'italic': 188 | isActive = activeFormats.includes('italic'); 189 | break; 190 | case 'code': 191 | // Disabled: code detection is unreliable in code blocks 192 | // isActive = activeFormats.includes('code'); 193 | isActive = false; 194 | break; 195 | case 'bulletList': 196 | isActive = activeFormats.includes('bullet-list'); 197 | break; 198 | case 'orderedList': 199 | isActive = activeFormats.includes('numbered-list'); 200 | break; 201 | case 'quote': 202 | isActive = activeFormats.includes('quote'); 203 | break; 204 | case 'taskList': 205 | isActive = activeFormats.includes('task-list'); 206 | break; 207 | case 'h1': 208 | isActive = activeFormats.includes('header'); 209 | break; 210 | case 'h2': 211 | isActive = activeFormats.includes('header-2'); 212 | break; 213 | case 'h3': 214 | isActive = activeFormats.includes('header-3'); 215 | break; 216 | case 'togglePlain': 217 | // Button is active when in overlay mode (not plain mode) 218 | isActive = !this.editor.container.classList.contains('plain-mode'); 219 | break; 220 | } 221 | 222 | button.classList.toggle('active', isActive); 223 | button.setAttribute('aria-pressed', isActive.toString()); 224 | }); 225 | } catch (error) { 226 | // Silently fail if markdown-actions not available 227 | } 228 | } 229 | 230 | /** 231 | * Toggle view mode dropdown menu 232 | */ 233 | toggleViewDropdown(button) { 234 | // Close any existing dropdown 235 | const existingDropdown = document.querySelector('.overtype-dropdown-menu'); 236 | if (existingDropdown) { 237 | existingDropdown.remove(); 238 | button.classList.remove('dropdown-active'); 239 | document.removeEventListener('click', this.handleDocumentClick); 240 | return; 241 | } 242 | 243 | // Create dropdown menu 244 | const dropdown = this.createViewDropdown(); 245 | 246 | // Position dropdown relative to button 247 | const rect = button.getBoundingClientRect(); 248 | dropdown.style.top = `${rect.bottom + 4}px`; 249 | dropdown.style.left = `${rect.left}px`; 250 | 251 | // Append to body instead of button 252 | document.body.appendChild(dropdown); 253 | button.classList.add('dropdown-active'); 254 | 255 | // Store reference for document click handler 256 | this.handleDocumentClick = (e) => { 257 | if (!button.contains(e.target) && !dropdown.contains(e.target)) { 258 | dropdown.remove(); 259 | button.classList.remove('dropdown-active'); 260 | document.removeEventListener('click', this.handleDocumentClick); 261 | } 262 | }; 263 | 264 | // Close on click outside 265 | setTimeout(() => { 266 | document.addEventListener('click', this.handleDocumentClick); 267 | }, 0); 268 | } 269 | 270 | /** 271 | * Create view mode dropdown menu 272 | */ 273 | createViewDropdown() { 274 | const dropdown = document.createElement('div'); 275 | dropdown.className = 'overtype-dropdown-menu'; 276 | 277 | // Determine current mode 278 | const isPlain = this.editor.container.classList.contains('plain-mode'); 279 | const isPreview = this.editor.container.classList.contains('preview-mode'); 280 | const currentMode = isPreview ? 'preview' : (isPlain ? 'plain' : 'normal'); 281 | 282 | // Create menu items 283 | const modes = [ 284 | { id: 'normal', label: 'Normal Edit', icon: '✓' }, 285 | { id: 'plain', label: 'Plain Textarea', icon: '✓' }, 286 | { id: 'preview', label: 'Preview Mode', icon: '✓' } 287 | ]; 288 | 289 | modes.forEach(mode => { 290 | const item = document.createElement('button'); 291 | item.className = 'overtype-dropdown-item'; 292 | item.type = 'button'; 293 | 294 | const check = document.createElement('span'); 295 | check.className = 'overtype-dropdown-check'; 296 | check.textContent = currentMode === mode.id ? mode.icon : ''; 297 | 298 | const label = document.createElement('span'); 299 | label.textContent = mode.label; 300 | 301 | item.appendChild(check); 302 | item.appendChild(label); 303 | 304 | if (currentMode === mode.id) { 305 | item.classList.add('active'); 306 | } 307 | 308 | item.addEventListener('click', (e) => { 309 | e.stopPropagation(); 310 | this.setViewMode(mode.id); 311 | dropdown.remove(); 312 | this.viewModeButton.classList.remove('dropdown-active'); 313 | document.removeEventListener('click', this.handleDocumentClick); 314 | }); 315 | 316 | dropdown.appendChild(item); 317 | }); 318 | 319 | return dropdown; 320 | } 321 | 322 | /** 323 | * Set view mode 324 | */ 325 | setViewMode(mode) { 326 | // Clear all mode classes 327 | this.editor.container.classList.remove('plain-mode', 'preview-mode'); 328 | 329 | switch(mode) { 330 | case 'plain': 331 | this.editor.showPlainTextarea(true); 332 | break; 333 | case 'preview': 334 | this.editor.showPreviewMode(true); 335 | break; 336 | case 'normal': 337 | default: 338 | // Normal edit mode 339 | this.editor.showPlainTextarea(false); 340 | if (typeof this.editor.showPreviewMode === 'function') { 341 | this.editor.showPreviewMode(false); 342 | } 343 | break; 344 | } 345 | } 346 | 347 | /** 348 | * Destroy toolbar 349 | */ 350 | destroy() { 351 | if (this.container) { 352 | // Clean up event listeners 353 | if (this.handleDocumentClick) { 354 | document.removeEventListener('click', this.handleDocumentClick); 355 | } 356 | this.container.remove(); 357 | this.container = null; 358 | this.buttons = {}; 359 | } 360 | } 361 | } -------------------------------------------------------------------------------- /test-types.ts: -------------------------------------------------------------------------------- 1 | // Test file to verify TypeScript definitions work correctly 2 | // Run: npx tsc --noEmit test-types.ts 3 | import OverType, { Theme, Options, Stats, OverType as OverTypeInstance } from './src/overtype'; 4 | 5 | // Test basic initialization - constructor returns array 6 | const editors1: OverTypeInstance[] = new OverType('#editor'); 7 | const editors2: OverTypeInstance[] = new OverType(document.getElementById('editor')!); 8 | const editors3: OverTypeInstance[] = new OverType('.editor-class'); 9 | 10 | // Test with comprehensive options 11 | const editorsWithOptions: OverTypeInstance[] = new OverType('#editor', { 12 | // Typography 13 | fontSize: '16px', 14 | lineHeight: 1.8, 15 | fontFamily: 'monospace', 16 | padding: '20px', 17 | 18 | // Mobile responsive 19 | mobile: { 20 | fontSize: '18px', 21 | padding: '10px', 22 | lineHeight: 1.5 23 | }, 24 | 25 | // Native textarea attributes (v1.1.2+) 26 | textareaProps: { 27 | required: true, 28 | maxLength: 500, 29 | name: 'markdown-content', 30 | 'data-form-field': 'content' 31 | }, 32 | 33 | // Behavior 34 | autofocus: true, 35 | autoResize: true, // v1.1.2+ 36 | minHeight: '200px', // v1.1.2+ 37 | maxHeight: '800px', // v1.1.2+ 38 | placeholder: 'Type here...', 39 | value: '# Initial content', 40 | 41 | // Features 42 | showActiveLineRaw: true, 43 | showStats: true, 44 | toolbar: true, 45 | statsFormatter: (stats: Stats) => `${stats.words} words, ${stats.chars} chars`, 46 | 47 | // Callbacks 48 | onChange: (value: string, instance: OverTypeInstance) => { 49 | console.log('Changed:', value); 50 | console.log('Instance:', instance.getValue()); 51 | }, 52 | onKeydown: (event: KeyboardEvent, instance: OverTypeInstance) => { 53 | if (event.key === 'Enter') { 54 | console.log('Enter pressed'); 55 | } 56 | } 57 | }); 58 | 59 | // Test instance methods 60 | if (editorsWithOptions.length > 0) { 61 | const instance = editorsWithOptions[0]; 62 | const value: string = instance.getValue(); 63 | instance.setValue('# New content'); 64 | const stats: Stats = instance.getStats(); 65 | const container: HTMLElement = instance.getContainer(); 66 | instance.focus(); 67 | instance.blur(); 68 | instance.updatePreview(); 69 | instance.showStats(true); 70 | instance.setTheme('cave'); 71 | instance.reinit({ fontSize: '18px' }); 72 | const isInit: boolean = instance.isInitialized(); 73 | instance.destroy(); 74 | } 75 | 76 | // Test static methods - init returns array 77 | const instances: OverTypeInstance[] = OverType.init('.editors', { toolbar: true }); 78 | const foundInstance: OverTypeInstance | null = OverType.getInstance(document.getElementById('editor')!); 79 | OverType.destroyAll(); 80 | OverType.setTheme('solar'); 81 | OverType.setTheme('cave', { text: '#fff', bgPrimary: '#000' }); 82 | 83 | // Test theme object 84 | const customTheme: Theme = { 85 | name: 'custom', 86 | colors: { 87 | bgPrimary: '#ffffff', 88 | bgSecondary: '#f0f0f0', 89 | text: '#333333', 90 | textSecondary: '#666666', 91 | h1: '#000000', 92 | h2: '#111111', 93 | h3: '#222222', 94 | strong: '#444444', 95 | em: '#555555', 96 | link: '#0066cc', 97 | code: '#666666', 98 | codeBg: '#f5f5f5', 99 | blockquote: '#777777', 100 | hr: '#888888', 101 | syntaxMarker: '#999999', 102 | listMarker: '#aaaaaa', 103 | cursor: '#ff0000', 104 | selection: 'rgba(0, 0, 255, 0.3)', 105 | rawLine: '#bbbbbb', 106 | toolbarBg: '#eeeeee', 107 | toolbarIcon: '#333333', 108 | toolbarHover: '#dddddd', 109 | toolbarActive: '#cccccc', 110 | border: '#dddddd' 111 | } 112 | }; 113 | OverType.setTheme(customTheme); 114 | 115 | // Test accessing built-in themes 116 | const solarTheme: Theme = OverType.themes.solar; 117 | const caveTheme: Theme = OverType.themes.cave; 118 | console.log('Solar theme:', solarTheme); 119 | console.log('Cave theme:', caveTheme); 120 | 121 | // Test Stats interface 122 | const statsExample: Stats = { 123 | words: 100, 124 | chars: 500, 125 | lines: 10, 126 | line: 5, 127 | column: 20 128 | }; 129 | 130 | // Test accessing properties 131 | if (editors1.length > 0) { 132 | const editor = editors1[0]; 133 | console.log('Container:', editor.container); 134 | console.log('Textarea:', editor.textarea); 135 | console.log('Preview:', editor.preview); 136 | console.log('Options:', editor.options); 137 | console.log('Initialized:', editor.initialized); 138 | console.log('Element:', editor.element); 139 | } 140 | 141 | // Test using static getTheme 142 | const fetchedTheme: Theme = OverType.getTheme('solar'); 143 | console.log('Fetched theme:', fetchedTheme); 144 | 145 | // Test accessing static properties 146 | console.log('Current theme:', OverType.currentTheme); 147 | console.log('Instance count:', OverType.instanceCount); 148 | 149 | console.log('TypeScript definitions test completed successfully!'); -------------------------------------------------------------------------------- /test/api-methods.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for OverType API methods 3 | * Tests getValue(), getRenderedHTML(), and getPreviewHTML() methods 4 | */ 5 | 6 | import { OverType } from '../src/overtype.js'; 7 | import { JSDOM } from 'jsdom'; 8 | 9 | // Set up DOM environment 10 | const dom = new JSDOM('
'); 11 | global.window = dom.window; 12 | global.document = dom.window.document; 13 | global.Element = dom.window.Element; 14 | global.NodeList = dom.window.NodeList; 15 | global.HTMLElement = dom.window.HTMLElement; 16 | global.performance = { now: () => Date.now() }; 17 | global.CSS = { supports: () => false }; // Mock CSS.supports for link-tooltip 18 | 19 | // Test results storage 20 | const results = { 21 | passed: 0, 22 | failed: 0, 23 | tests: [] 24 | }; 25 | 26 | // Helper function for assertions 27 | function assert(condition, testName, message) { 28 | if (condition) { 29 | results.passed++; 30 | results.tests.push({ name: testName, passed: true }); 31 | console.log(`✓ ${testName}`); 32 | } else { 33 | results.failed++; 34 | results.tests.push({ name: testName, passed: false, message }); 35 | console.error(`✗ ${testName}: ${message}`); 36 | } 37 | } 38 | 39 | // Test Suite 40 | console.log('🧪 Running API Methods Tests...\n'); 41 | console.log('━'.repeat(50)); 42 | 43 | // ===== API Methods Tests ===== 44 | console.log('\n📚 API Methods Tests\n'); 45 | 46 | // Test: getValue() method 47 | (() => { 48 | const editor = new OverType('#editor')[0]; 49 | const testContent = '# Hello World\n\nThis is **bold** text.'; 50 | editor.setValue(testContent); 51 | 52 | const value = editor.getValue(); 53 | assert(value === testContent, 'getValue()', `Should return current markdown content`); 54 | })(); 55 | 56 | // Test: setValue() method 57 | (() => { 58 | const editor = new OverType('#editor')[0]; 59 | const testContent = '## Test Header\n\n*Italic* text here.'; 60 | editor.setValue(testContent); 61 | 62 | assert(editor.textarea.value === testContent, 'setValue()', `Should update textarea value`); 63 | assert(editor.preview.innerHTML.includes('

'), 'setValue() updates preview', `Should update preview HTML`); 64 | })(); 65 | 66 | // Test: getRenderedHTML() without post-processing 67 | (() => { 68 | const editor = new OverType('#editor')[0]; 69 | const markdown = '# Title\n\n**Bold** and *italic*'; 70 | editor.setValue(markdown); 71 | 72 | const html = editor.getRenderedHTML(false); 73 | assert(html.includes('

'), 'getRenderedHTML() has h1', `Should render h1 tag`); 74 | assert(html.includes(''), 'getRenderedHTML() has strong', `Should render strong tag`); 75 | assert(html.includes(''), 'getRenderedHTML() has em', `Should render em tag`); 76 | assert(!html.includes('
'), 'getRenderedHTML() no post-processing', `Should not have consolidated code blocks`);
 77 | })();
 78 | 
 79 | // Test: getRenderedHTML() with post-processing
 80 | (() => {
 81 |   const editor = new OverType('#editor')[0];
 82 |   const markdown = '```\ncode block\n```';
 83 |   editor.setValue(markdown);
 84 |   
 85 |   const html = editor.getRenderedHTML(true);
 86 |   assert(html.includes('
'), 'getRenderedHTML(true) post-processes', `Should have consolidated code blocks`);
 87 | })();
 88 | 
 89 | // Test: getPreviewHTML() method
 90 | (() => {
 91 |   const editor = new OverType('#editor')[0];
 92 |   const markdown = '### Header 3\n\n[Link](https://example.com)';
 93 |   editor.setValue(markdown);
 94 |   
 95 |   const previewHTML = editor.getPreviewHTML();
 96 |   assert(previewHTML.includes('

'), 'getPreviewHTML() has h3', `Should contain h3 from preview`); 97 | assert(previewHTML.includes(' { 102 | const editor = new OverType('#editor')[0]; 103 | const markdown = `# Main Title 104 | 105 | ## Subtitle 106 | 107 | This has **bold**, *italic*, and \`inline code\`. 108 | 109 | \`\`\`javascript 110 | const x = 42; 111 | \`\`\` 112 | 113 | - List item 1 114 | - List item 2 115 | 116 | [Link text](https://test.com)`; 117 | 118 | editor.setValue(markdown); 119 | 120 | const value = editor.getValue(); 121 | const renderedHTML = editor.getRenderedHTML(false); 122 | const renderedHTMLProcessed = editor.getRenderedHTML(true); 123 | const previewHTML = editor.getPreviewHTML(); 124 | 125 | // Test getValue returns original markdown 126 | assert(value === markdown, 'Complex: getValue()', `Should return original markdown`); 127 | 128 | // Test rendered HTML contains expected elements 129 | assert(renderedHTML.includes('

'), 'Complex: rendered has h1', `Should have h1`); 130 | assert(renderedHTML.includes('

'), 'Complex: rendered has h2', `Should have h2`); 131 | assert(renderedHTML.includes(''), 'Complex: rendered has strong', `Should have strong`); 132 | assert(renderedHTML.includes(''), 'Complex: rendered has em', `Should have em`); 133 | assert(renderedHTML.includes(''), 'Complex: rendered has code', `Should have code`); 134 | assert(renderedHTML.includes('