/** * TestDrive-JSUI - JavaScript-First Markdown Editor * * Main entry point for the TestDrive-JSUI library. * This is a pure JavaScript library for interactive markdown editing. * Language adapters (Python, Ruby, Java) are optional integration helpers. * * @version 1.0.0 * @license MIT */ /** * TestDriveJSUI - Main library class * * Usage: * ```javascript * const editor = new TestDriveJSUI({ * container: '#editor', * markdown: '# Hello World\n\nEdit me!', * mode: 'edit', * theme: 'github', * autosave: false * }); * ``` */ class TestDriveJSUI { constructor(options = {}) { // Validate required options if (!options.container) { throw new Error('TestDriveJSUI: container option is required'); } // Configuration this.config = { container: options.container, markdown: options.markdown || '# Welcome\n\nStart editing...', mode: options.mode || 'edit', // 'edit' or 'view' theme: options.theme || 'github', autosave: options.autosave || false, shortcuts: options.shortcuts !== false, // default true sections: options.sections !== false, // default true debug: options.debug || false, // Advanced control panels (compass-positioned) controls: { editControl: options.controls?.editControl !== false, // default true statusControl: options.controls?.statusControl !== false, // default true contentsControl: options.controls?.contentsControl !== false, // default true debugControl: options.controls?.debugControl || false, // default false // Legacy simple control panel (top-right box) simpleControls: options.controls?.simpleControls || false // default false }, // Control positioning (compass: nw, ne, e, se, s, sw, w) controlPositions: { editControl: options.controlPositions?.editControl || 'ne', statusControl: options.controlPositions?.statusControl || 'e', contentsControl: options.controlPositions?.contentsControl || 'nw', debugControl: options.controlPositions?.debugControl || 'se' }, ...options }; // Internal state this.container = null; this.sectionManager = null; this.domRenderer = null; this.documentControls = null; // Advanced control panel instances this.editControl = null; this.statusControl = null; this.contentsControl = null; this.debugControl = null; this.isInitialized = false; this.listeners = new Map(); // Initialize this.init(); } /** * Initialize the editor */ init() { if (this.isInitialized) { console.warn('TestDriveJSUI: Already initialized'); return; } // Resolve container this.container = this.resolveContainer(this.config.container); if (!this.container) { throw new Error('TestDriveJSUI: Could not resolve container'); } // Check for marked.js dependency if (typeof marked === 'undefined') { console.warn('TestDriveJSUI: marked.js not loaded. Markdown rendering will be limited.'); } // Apply theme this.applyTheme(this.config.theme); // Initialize components based on mode if (this.config.mode === 'edit') { this.initEditMode(); } else { this.initViewMode(); } this.isInitialized = true; // Emit initialized event this.emit('initialized', { mode: this.config.mode, theme: this.config.theme, sections: this.sectionManager ? this.sectionManager.sections.size : 0 }); } /** * Initialize edit mode with full interactivity */ initEditMode() { // Load required components if (typeof SectionManager === 'undefined') { console.error('TestDriveJSUI: SectionManager not loaded'); return; } if (typeof DOMRenderer === 'undefined') { console.error('TestDriveJSUI: DOMRenderer not loaded'); return; } // Create section manager this.sectionManager = new SectionManager(); // Create DOM renderer this.domRenderer = new DOMRenderer(this.sectionManager, this.container); // Initialize control panels based on configuration if (this.config.controls.simpleControls) { // Legacy simple controls (top-right box) if (typeof DocumentControls === 'undefined') { console.warn('TestDriveJSUI: DocumentControls not loaded'); } else { this.documentControls = new DocumentControls(); this.documentControls.create(); this.setupControlHandlers(); } } else { // Advanced compass-positioned control panels this.initializeAdvancedControls(); } // Parse markdown into sections and render this.sectionManager.createSectionsFromMarkdown(this.config.markdown); // Setup keyboard shortcuts if enabled if (this.config.shortcuts) { this.setupKeyboardShortcuts(); } // Setup autosave if enabled if (this.config.autosave) { this.setupAutosave(); } } /** * Initialize view mode (read-only rendering) */ initViewMode() { // Clear container this.container.innerHTML = ''; // Render markdown directly using marked.js or simple renderer const html = this.renderMarkdown(this.config.markdown); // Create content wrapper const contentWrapper = document.createElement('div'); contentWrapper.className = 'testdrive-view-content'; contentWrapper.innerHTML = html; this.container.appendChild(contentWrapper); // Apply view mode styling contentWrapper.style.cssText = ` padding: 20px; max-width: 800px; margin: 0 auto; line-height: 1.6; `; } /** * Setup control panel event handlers */ setupControlHandlers() { this.documentControls.setEventHandlers({ 'save-document': () => { const markdown = this.getMarkdown(); this.emit('save', { markdown }); // If autosave is enabled and we have a save handler, call it if (this.listeners.has('save')) { // User has registered a save handler } else { // Default: save to localStorage this.saveToLocalStorage(markdown); alert('Document saved to browser localStorage'); } }, 'reset-all': () => { if (confirm('Reset all sections to original content?')) { this.resetAll(); } }, 'show-status': () => { const status = this.getStatus(); this.showStatus(status); }, 'toggle-debug': () => { this.toggleDebug(); } }); } /** * Initialize advanced compass-positioned control panels */ initializeAdvancedControls() { console.log('🎛️ Initializing advanced control panels...'); // ContentsControl (Table of Contents) if (this.config.controls.contentsControl && typeof ContentsControl !== 'undefined') { this.contentsControl = new ContentsControl(); this.contentsControl.config.position = this.config.controlPositions.contentsControl; this.contentsControl.show(); console.log(`✅ ContentsControl initialized (${this.config.controlPositions.contentsControl})`); } // StatusControl (Document Statistics) if (this.config.controls.statusControl && typeof StatusControl !== 'undefined') { this.statusControl = new StatusControl(); this.statusControl.config.position = this.config.controlPositions.statusControl; this.statusControl.show(); console.log(`✅ StatusControl initialized (${this.config.controlPositions.statusControl})`); } // DebugControl (Debug Logs) if (this.config.controls.debugControl && typeof DebugControl !== 'undefined') { this.debugControl = new DebugControl(); this.debugControl.config.position = this.config.controlPositions.debugControl; this.debugControl.show(); console.log(`✅ DebugControl initialized (${this.config.controlPositions.debugControl})`); } // EditControl (Document Actions) if (this.config.controls.editControl && typeof EditControl !== 'undefined') { this.editControl = new EditControl(); this.editControl.config.position = this.config.controlPositions.editControl; this.editControl.show(); console.log(`✅ EditControl initialized (${this.config.controlPositions.editControl})`); } // Setup keyboard shortcuts if enabled if (this.config.shortcuts) { this.setupKeyboardShortcuts(); } // Setup autosave if enabled if (this.config.autosave) { this.setupAutosave(); } } /** * Setup keyboard shortcuts */ setupKeyboardShortcuts() { document.addEventListener('keydown', (event) => { // Ctrl+S or Cmd+S - Save if ((event.ctrlKey || event.metaKey) && event.key === 's') { event.preventDefault(); this.save(); } // Escape - Close editor if (event.key === 'Escape') { if (this.domRenderer && this.domRenderer.currentFloatingMenu) { this.domRenderer.hideCurrentEditor(); } } }); } /** * Setup autosave functionality */ setupAutosave() { // Save every 30 seconds this.autosaveInterval = setInterval(() => { const markdown = this.getMarkdown(); this.saveToLocalStorage(markdown); this.emit('autosave', { markdown }); }, 30000); } /** * Resolve container from selector or element */ resolveContainer(container) { if (typeof container === 'string') { return document.querySelector(container); } else if (container instanceof HTMLElement) { return container; } return null; } /** * Render markdown to HTML */ renderMarkdown(markdown) { if (typeof marked !== 'undefined') { // Use marked.js for full markdown support marked.setOptions({ gfm: true, breaks: true, headerIds: true, mangle: false, sanitize: false }); return marked.parse(markdown); } else { // Fallback to simple rendering return this.simpleMarkdownRender(markdown); } } /** * Simple markdown renderer (fallback) */ simpleMarkdownRender(markdown) { return markdown .replace(/^# (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^### (.*$)/gim, '

$1

') .replace(/!\[(.*?)\]\((.*?)\)/gim, '$1') .replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '$1') .replace(/\*\*(.*?)\*\*/gim, '$1') .replace(/\*(.*?)\*/gim, '$1') .replace(/`([^`]+)`/gim, '$1') .replace(/\n/gim, '
'); } /** * Apply theme to the editor */ applyTheme(themeName) { // Remove existing theme classes this.container.className = this.container.className .split(' ') .filter(c => !c.startsWith('theme-')) .join(' '); // Add new theme class this.container.classList.add(`theme-${themeName}`); // Apply base styles this.container.style.cssText = ` font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; line-height: 1.6; color: #24292e; background: #ffffff; min-height: 300px; `; } /** * Get current markdown content */ getMarkdown() { if (this.sectionManager) { return this.sectionManager.getDocumentMarkdown(); } return this.config.markdown; } /** * Set markdown content */ setMarkdown(markdown) { this.config.markdown = markdown; if (this.config.mode === 'edit' && this.sectionManager) { // Re-parse and render sections this.sectionManager.sections.clear(); this.sectionManager.createSectionsFromMarkdown(markdown); } else { // Re-render view mode this.container.innerHTML = ''; this.initViewMode(); } this.emit('content-changed', { markdown }); } /** * Get editor status */ getStatus() { if (this.sectionManager) { const sections = this.sectionManager.getAllSections(); const editingSections = sections.filter(s => s.isEditing()); const modifiedSections = sections.filter(s => s.hasChanges()); return { mode: this.config.mode, totalSections: sections.length, editingSections: editingSections.length, modifiedSections: modifiedSections.length, wordCount: this.getWordCount(), characterCount: this.getMarkdown().length }; } return { mode: this.config.mode, wordCount: this.getWordCount(), characterCount: this.config.markdown.length }; } /** * Get word count */ getWordCount() { const text = this.getMarkdown(); return text.split(/\s+/).filter(word => word.length > 0).length; } /** * Show status dialog */ showStatus(status) { const message = [ `Mode: ${status.mode}`, `Total Sections: ${status.totalSections || 0}`, `Editing: ${status.editingSections || 0}`, `Modified: ${status.modifiedSections || 0}`, `Words: ${status.wordCount}`, `Characters: ${status.characterCount}` ].join('\n'); alert(message); } /** * Save current content */ save() { const markdown = this.getMarkdown(); this.emit('save', { markdown }); // Default: save to localStorage this.saveToLocalStorage(markdown); } /** * Save to browser localStorage */ saveToLocalStorage(markdown) { try { localStorage.setItem('testdrive-jsui-content', markdown); localStorage.setItem('testdrive-jsui-timestamp', new Date().toISOString()); } catch (e) { console.error('Failed to save to localStorage:', e); } } /** * Load from browser localStorage */ loadFromLocalStorage() { try { const markdown = localStorage.getItem('testdrive-jsui-content'); if (markdown) { this.setMarkdown(markdown); return true; } } catch (e) { console.error('Failed to load from localStorage:', e); } return false; } /** * Download markdown as file */ download(filename = 'document.md') { const markdown = this.getMarkdown(); const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.emit('download', { filename, markdown }); } /** * Reset all sections to original content */ resetAll() { if (this.sectionManager) { const sections = this.sectionManager.getAllSections(); sections.forEach(section => { this.sectionManager.resetSection(section.id); }); } this.emit('reset'); } /** * Toggle debug mode */ toggleDebug() { this.config.debug = !this.config.debug; const debugContainer = document.getElementById('debug-messages-container'); if (debugContainer) { debugContainer.style.display = this.config.debug ? 'block' : 'none'; } this.emit('debug-toggled', { enabled: this.config.debug }); } /** * Event listener system */ on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); return this; } /** * Remove event listener */ off(event, callback) { if (this.listeners.has(event)) { const callbacks = this.listeners.get(event); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } return this; } /** * Emit event */ emit(event, data) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => { try { callback(data); } catch (e) { console.error(`Error in event listener for '${event}':`, e); } }); } } /** * Destroy the editor and clean up */ destroy() { // Clear autosave interval if (this.autosaveInterval) { clearInterval(this.autosaveInterval); } // Destroy simple document controls if (this.documentControls) { this.documentControls.destroy(); } // Destroy advanced control panels if (this.editControl) { this.editControl.destroy(); } if (this.statusControl) { this.statusControl.destroy(); } if (this.contentsControl) { this.contentsControl.destroy(); } if (this.debugControl) { this.debugControl.destroy(); } // Clear container if (this.container) { this.container.innerHTML = ''; } // Clear references this.sectionManager = null; this.domRenderer = null; this.documentControls = null; this.editControl = null; this.statusControl = null; this.contentsControl = null; this.debugControl = null; this.listeners.clear(); this.isInitialized = false; this.emit('destroyed'); } } // Export for CommonJS (Node.js, Jest) if (typeof module !== 'undefined' && module.exports) { module.exports = { TestDriveJSUI }; } // Export for browser use if (typeof window !== 'undefined') { window.TestDriveJSUI = TestDriveJSUI; }