feat: Complete Phase 4 - Remove legacy JavaScript files

Phase 4 Complete: Cleanup legacy files after successful migration

Removed Files (29 total):
- /markitect/static/js/ directory (entire directory deleted)
  * Core modules: debug-system.js, section-manager.js
  * Components: debug-panel.js, dom-renderer.js, document-controls.js
  * Configuration: config-loader.js
  * Main files: main.js, main-updated.js
  * Plugins: document-navigator-plugin.js
  * Widgets: UIWidget.js, Widget.js, DocumentNavigator.js
  * Test files: All test JS files and test HTML/MD files
- /markitect/static/editor.js (unused legacy file)

Preserved:
- /markitect/static/css/ (still referenced in templates)

Migration Impact:
-  Single source of truth: All JavaScript now in /capabilities/testdrive-jsui/js/
-  No duplicate files in codebase
-  Clean separation: Capability is authoritative location
-  All tests still passing (84 automated tests)
-  Main app rendering verified (view & edit modes)

Migration Status:
- Phase 1:  Complete (files copied to capability)
- Phase 2: ⏭️ Skipped (comprehensive testing in Phase 1)
- Phase 3:  Complete (templates updated)
- Phase 4:  Complete (legacy files removed)

🎉 MIGRATION FULLY COMPLETE - All phases done

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-16 10:27:05 +01:00
parent e84eb08dc5
commit d592c5b8b3
30 changed files with 1 additions and 13379 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,191 +0,0 @@
/**
* DebugPanel Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Handles debug message display and management for client-side debugging.
*
* Dependencies:
* - None (standalone component)
*/
/**
* DebugPanel - Manages debug message display and interaction
*/
class DebugPanel {
constructor() {
this.messages = [];
this.isActive = false;
this.maxMessages = 1000; // Keep last 1000 messages
}
/**
* Add a debug message
*/
addMessage(message, category = 'INFO') {
const messageObj = {
message,
category,
timestamp: new Date().toLocaleTimeString()
};
this.messages.push(messageObj);
// Keep only last maxMessages
if (this.messages.length > this.maxMessages) {
this.messages = this.messages.slice(-this.maxMessages);
}
// Auto-update if panel is visible
if (this.isActive) {
this.update();
}
}
/**
* Toggle the debug panel on/off
*/
toggle() {
const debugContainer = document.getElementById('debug-messages-container');
const debugButton = document.getElementById('toggle-debug');
if (!debugContainer || !debugButton) {
console.warn('DebugPanel: Required DOM elements not found');
return;
}
if (this.isActive) {
this.hide();
} else {
this.show();
}
}
/**
* Show the debug panel
*/
show() {
const debugContainer = document.getElementById('debug-messages-container');
const debugButton = document.getElementById('toggle-debug');
if (!debugContainer || !debugButton) {
console.warn('DebugPanel: Required DOM elements not found');
return;
}
debugContainer.style.display = 'block';
debugButton.textContent = '🔍 Debug (ON)';
debugButton.style.background = '#28a745';
this.isActive = true;
this.update();
}
/**
* Hide the debug panel
*/
hide() {
const debugContainer = document.getElementById('debug-messages-container');
const debugButton = document.getElementById('toggle-debug');
if (!debugContainer || !debugButton) {
console.warn('DebugPanel: Required DOM elements not found');
return;
}
debugContainer.style.display = 'none';
debugButton.textContent = '🔍 Debug';
debugButton.style.background = '#6c757d';
this.isActive = false;
}
/**
* Update the debug panel with current messages
*/
update() {
const debugContainer = document.getElementById('debug-messages-container');
if (!debugContainer || !this.isActive) {
return;
}
if (this.messages.length === 0) {
debugContainer.innerHTML = '<div style="color: #6c757d; font-style: italic; padding: 12px;">No debug messages yet. Click sections to generate debug output.</div>';
return;
}
// Show the last 50 messages in reverse order (newest first)
const recentMessages = this.messages.slice(-50).reverse();
const messagesHtml = recentMessages.map(msg => {
const categoryColor = {
'INFO': '#17a2b8',
'WARNING': '#ffc107',
'ERROR': '#dc3545',
'SUCCESS': '#28a745',
'DEBUG': '#6f42c1'
}[msg.category] || '#6c757d';
return `
<div style="margin-bottom: 6px; padding: 4px; border-left: 3px solid ${categoryColor}; background: white; border-radius: 2px;">
<span style="color: #6c757d; font-size: 11px;">[${msg.timestamp}]</span>
<span style="color: ${categoryColor}; font-weight: bold;">${msg.category}:</span>
<span style="color: #333;">${msg.message}</span>
</div>
`;
}).join('');
debugContainer.innerHTML = `
<div style="margin-bottom: 8px; padding: 6px; background: #e9ecef; border-radius: 4px; font-weight: bold; color: #495057;">
Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
<button id="debug-clear-btn" style="float: right; background: #dc3545; color: white; border: none; padding: 2px 6px; border-radius: 2px; font-size: 11px; cursor: pointer;">Clear</button>
</div>
<div style="max-height: 250px; overflow-y: auto;">
${messagesHtml}
</div>
`;
// Add event listener for clear button
const clearBtn = debugContainer.querySelector('#debug-clear-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
this.clear();
});
}
// Auto-scroll to bottom to show newest messages
const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
}
/**
* Clear all debug messages
*/
clear() {
this.messages = [];
this.update();
}
/**
* Get the number of messages
*/
getMessageCount() {
return this.messages.length;
}
/**
* Get recent messages
*/
getRecentMessages(count = 10) {
return this.messages.slice(-count);
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DebugPanel };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.DebugPanel = DebugPanel;
}

View File

@@ -1,279 +0,0 @@
/**
* DocumentControls Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Handles the floating control panel and document-level actions.
*
* Dependencies:
* - None (standalone component)
*/
/**
* DocumentControls - Manages the floating control panel and its buttons
*/
class DocumentControls {
constructor() {
this.controlPanel = null;
this.buttons = new Map();
this.eventHandlers = new Map();
this.isVisible = true;
}
/**
* Create the control panel and add it to the DOM
*/
create() {
if (this.controlPanel) {
this.destroy(); // Remove existing panel
}
// Also remove any existing panel with the same ID in the DOM
const existingPanel = document.getElementById('markitect-global-controls');
if (existingPanel && existingPanel.parentNode) {
existingPanel.parentNode.removeChild(existingPanel);
}
// Create the floating control panel
this.controlPanel = document.createElement('div');
this.controlPanel.id = 'markitect-global-controls';
this.controlPanel.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(248, 249, 250, 0.95);
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
backdrop-filter: blur(8px);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
min-width: 200px;
`;
// Add title
const title = document.createElement('div');
title.style.cssText = `
font-weight: 600;
margin-bottom: 8px;
color: #495057;
border-bottom: 1px solid #dee2e6;
padding-bottom: 4px;
`;
title.textContent = 'Document Controls';
// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.id = 'button-container';
buttonContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 6px;
`;
this.controlPanel.appendChild(title);
this.controlPanel.appendChild(buttonContainer);
// Add default buttons
this.addDefaultButtons();
// Add debug messages container
this.addDebugContainer();
// Add to DOM
document.body.appendChild(this.controlPanel);
}
/**
* Add default buttons to the control panel
*/
addDefaultButtons() {
// Save Document button
this.addButton('save-document', '💾 Save Document', '#28a745');
// Reset All button
this.addButton('reset-all', '🔄 Reset All', '#ffc107', '#212529');
// Show Status button
this.addButton('show-status', '📊 Show Status', '#17a2b8');
// Debug button
this.addButton('toggle-debug', '🔍 Debug', '#6c757d');
}
/**
* Add debug container to the control panel
*/
addDebugContainer() {
const debugContainer = document.createElement('div');
debugContainer.id = 'debug-messages-container';
debugContainer.style.cssText = `
margin-top: 12px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #f8f9fa;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
display: none;
`;
this.controlPanel.appendChild(debugContainer);
}
/**
* Add a button to the control panel
*/
addButton(id, text, backgroundColor, textColor = 'white') {
const buttonContainer = this.controlPanel.querySelector('#button-container');
if (!buttonContainer) {
throw new Error('Button container not found. Call create() first.');
}
const button = document.createElement('button');
button.id = id;
button.textContent = text;
button.style.cssText = `
background: ${backgroundColor};
color: ${textColor};
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
`;
buttonContainer.appendChild(button);
this.buttons.set(id, button);
return button;
}
/**
* Remove a button from the control panel
*/
removeButton(id) {
const button = this.buttons.get(id);
if (button && button.parentNode) {
button.parentNode.removeChild(button);
this.buttons.delete(id);
this.eventHandlers.delete(id);
}
}
/**
* Set event handlers for buttons
*/
setEventHandlers(handlers) {
for (const [buttonId, handler] of Object.entries(handlers)) {
const button = this.buttons.get(buttonId);
if (button) {
// Remove existing handler if any
if (this.eventHandlers.has(buttonId)) {
button.removeEventListener('click', this.eventHandlers.get(buttonId));
}
// Add new handler
button.addEventListener('click', handler);
this.eventHandlers.set(buttonId, handler);
}
}
}
/**
* Show the control panel
*/
show() {
if (this.controlPanel) {
this.controlPanel.style.display = 'block';
this.isVisible = true;
}
}
/**
* Hide the control panel
*/
hide() {
if (this.controlPanel) {
this.controlPanel.style.display = 'none';
this.isVisible = false;
}
}
/**
* Update status display (can be extended as needed)
*/
updateStatus(status) {
// This method can be extended to show status information
// For now, it just stores the status for potential display
this.lastStatus = status;
// Could update a status indicator in the panel if needed
if (status && this.controlPanel) {
const title = this.controlPanel.querySelector('div');
if (title) {
const statusText = `Document Controls (${status.totalSections} sections, ${status.editingSections} editing)`;
// Could update title or add status indicator
}
}
}
/**
* Get the control panel element
*/
getControlPanel() {
return this.controlPanel;
}
/**
* Destroy the control panel and clean up
*/
destroy() {
if (this.controlPanel && this.controlPanel.parentNode) {
this.controlPanel.parentNode.removeChild(this.controlPanel);
}
// Clean up references
this.controlPanel = null;
this.buttons.clear();
this.eventHandlers.clear();
this.isVisible = true;
}
/**
* Check if the control panel is visible
*/
isVisible() {
return this.isVisible && this.controlPanel && this.controlPanel.style.display !== 'none';
}
/**
* Get all button IDs
*/
getButtonIds() {
return Array.from(this.buttons.keys());
}
/**
* Get a specific button by ID
*/
getButton(id) {
return this.buttons.get(id);
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DocumentControls };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.DocumentControls = DocumentControls;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,168 +0,0 @@
/**
* Configuration Loader - Clean interface between Python and JavaScript
*
* This module provides the ONLY interface for Python-generated data.
* All dynamic data from Python must be passed through this JSON configuration.
*/
class MarkitectConfig {
constructor() {
this.config = null;
this.loaded = false;
// Simple immediate loading - if script is loaded, DOM is ready
this.loadConfig();
}
loadConfig() {
try {
const configElement = document.getElementById('markitect-config');
if (!configElement) {
throw new Error('Markitect configuration not found - missing markitect-config script element');
}
this.config = JSON.parse(configElement.textContent);
this.loaded = true;
console.log('✅ Markitect configuration loaded successfully');
// Validate required fields
this.validateConfig();
} catch (error) {
console.error('❌ Failed to load Markitect configuration:', error);
this.config = this.getDefaultConfig();
}
}
validateConfig() {
const required = ['markdownContent', 'mode'];
const missing = required.filter(key => !(key in this.config));
if (missing.length > 0) {
console.warn('⚠️ Missing required config fields:', missing);
}
}
getDefaultConfig() {
return {
markdownContent: '# Default Content\n\nConfiguration failed to load.',
markdownContentWithDogtag: '# Default Content\n\nConfiguration failed to load.',
dogtagContent: '',
mode: 'edit',
theme: 'github',
keyboardShortcuts: true,
autosave: false,
sections: true,
originalFilename: 'document',
version: 'Markitect v0.8.1',
repoName: 'Markitect',
base64References: {}
};
}
// Getter methods for clean access
get markdownContent() {
return this.config.markdownContent || '';
}
get markdownContentWithDogtag() {
return this.config.markdownContentWithDogtag || this.markdownContent;
}
get dogtagContent() {
return this.config.dogtagContent || '';
}
get mode() {
return this.config.mode || 'edit';
}
get isEditMode() {
return this.mode === 'edit';
}
get isInsertMode() {
return this.mode === 'insert';
}
get theme() {
return this.config.theme || 'github';
}
get originalFilename() {
return this.config.originalFilename || 'document';
}
get version() {
return this.config.version || 'Markitect v0.8.1';
}
get repoName() {
return this.config.repoName || 'Markitect';
}
get keyboardShortcuts() {
return this.config.keyboardShortcuts !== false;
}
get base64References() {
return this.config.base64References || {};
}
get restrictedHeadingLevels() {
return this.config.restrictedHeadingLevels || [1, 2, 3];
}
// Check if config is ready for access
isReady() {
return this.loaded && this.config !== null;
}
// Wait for config to be ready
waitForReady(callback, maxWait = 5000) {
const startTime = Date.now();
const checkReady = () => {
if (this.isReady()) {
callback();
} else if (Date.now() - startTime < maxWait) {
setTimeout(checkReady, 50);
} else {
console.error('❌ Configuration loading timeout after', maxWait, 'ms');
callback(); // Call anyway with default config
}
};
checkReady();
}
// Get full editor configuration object
getEditorConfig() {
if (!this.isReady()) {
console.warn('⚠️ Configuration not ready, using defaults');
return this.getDefaultConfig();
}
return {
mode: this.mode,
theme: this.theme,
keyboardShortcuts: this.keyboardShortcuts,
autosave: this.config.autosave || false,
sections: this.config.sections !== false,
originalFilename: this.originalFilename,
version: this.version,
repoName: this.repoName,
restrictedHeadingLevels: this.restrictedHeadingLevels
};
}
}
// Global configuration instance
window.markitectConfig = new MarkitectConfig();
// Legacy compatibility - expose common config values globally
window.editorConfig = window.markitectConfig.getEditorConfig();
window.markitectBase64References = window.markitectConfig.base64References;
// Export for module use
if (typeof module !== 'undefined' && module.exports) {
module.exports = MarkitectConfig;
}

View File

@@ -1,290 +0,0 @@
/**
* Independent Debug System for Markitect
* Uses IndexedDB for persistence and provides selection-based filtering
*/
class MarkitectDebugSystem {
constructor() {
this.db = null;
this.messages = [];
this.maxMessages = 1000;
this.isEnabled = true;
this.subscribers = [];
// Selection and filtering system
this.selectionCriteria = {
includeDocumentEvents: true,
includeSystemEvents: false,
includeControlEvents: true,
includeEditingEvents: true,
includeNavigationEvents: false,
includedHeadings: new Set(), // Track which document headings to monitor
excludedSources: new Set(['ContentsControl', 'DocumentNavigator'])
};
this.init();
}
// Initialize IndexedDB for persistence
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MarkitectDebugDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
this.loadMessages().then(resolve);
};
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('messages')) {
const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('category', 'category', { unique: false });
}
};
});
}
// Add a debug message with selection filtering
async addMessage(message, category = 'INFO', source = 'System', context = {}) {
// Check if this message should be included based on selection criteria
if (!this.shouldIncludeMessage(message, category, source, context)) {
return null;
}
const messageObj = {
timestamp: new Date().toISOString(),
message: String(message),
category: category.toUpperCase(),
source: String(source),
context: context || {},
id: null // Will be set by IndexedDB
};
// Store in IndexedDB if available
if (this.db) {
try {
await this.saveMessage(messageObj);
} catch (error) {
console.warn('Failed to save debug message to IndexedDB:', error);
}
}
// Store in memory
this.messages.unshift(messageObj);
// Limit memory storage
if (this.messages.length > this.maxMessages) {
this.messages = this.messages.slice(0, this.maxMessages);
}
// Notify subscribers
this.notifySubscribers(messageObj);
// Console output for development
const consoleMethod = category.toLowerCase() === 'error' ? 'error' :
category.toLowerCase() === 'warning' ? 'warn' : 'log';
console[consoleMethod](`[${source}] ${message}`, context);
return messageObj;
}
// Selection filtering logic
shouldIncludeMessage(message, category, source, context) {
if (!this.isEnabled) return false;
const eventType = context.eventType || 'UNKNOWN';
const criteria = this.selectionCriteria;
// Check event type filters
switch (eventType.toUpperCase()) {
case 'DOCUMENT':
if (!criteria.includeDocumentEvents) return false;
break;
case 'SYSTEM':
if (!criteria.includeSystemEvents) return false;
break;
case 'CONTROL':
if (!criteria.includeControlEvents) return false;
break;
case 'EDITING':
if (!criteria.includeEditingEvents) return false;
break;
case 'NAVIGATION':
if (!criteria.includeNavigationEvents) return false;
break;
}
// Check excluded sources
if (criteria.excludedSources.has(source)) {
return false;
}
// Check heading-specific filtering
if (context.sectionId && criteria.includedHeadings.size > 0) {
const sectionElement = document.getElementById(context.sectionId);
if (sectionElement) {
const heading = sectionElement.querySelector('h1, h2, h3, h4, h5, h6');
if (heading && !criteria.includedHeadings.has(heading.textContent.trim())) {
return false;
}
}
}
return true;
}
// Save message to IndexedDB
async saveMessage(messageObj) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['messages'], 'readwrite');
const store = transaction.objectStore('messages');
const request = store.add(messageObj);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Load messages from IndexedDB
async loadMessages() {
if (!this.db) return [];
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['messages'], 'readonly');
const store = transaction.objectStore('messages');
const request = store.getAll();
request.onsuccess = () => {
this.messages = request.result.reverse(); // Most recent first
resolve(this.messages);
};
request.onerror = () => reject(request.error);
});
}
// Clear all messages
async clearMessages() {
this.messages = [];
if (this.db) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['messages'], 'readwrite');
const store = transaction.objectStore('messages');
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// Get filtered messages
getMessages(filter = {}) {
let filteredMessages = [...this.messages];
if (filter.category) {
filteredMessages = filteredMessages.filter(msg =>
msg.category.toLowerCase() === filter.category.toLowerCase()
);
}
if (filter.source) {
filteredMessages = filteredMessages.filter(msg =>
msg.source.toLowerCase().includes(filter.source.toLowerCase())
);
}
if (filter.since) {
const sinceDate = new Date(filter.since);
filteredMessages = filteredMessages.filter(msg =>
new Date(msg.timestamp) >= sinceDate
);
}
if (filter.limit) {
filteredMessages = filteredMessages.slice(0, filter.limit);
}
return filteredMessages;
}
// Update selection criteria
updateSelectionCriteria(updates) {
Object.assign(this.selectionCriteria, updates);
this.notifySubscribers({ type: 'criteria-updated', criteria: this.selectionCriteria });
}
// Add heading to monitoring
addHeadingToMonitoring(headingText) {
this.selectionCriteria.includedHeadings.add(headingText);
}
// Remove heading from monitoring
removeHeadingFromMonitoring(headingText) {
this.selectionCriteria.includedHeadings.delete(headingText);
}
// Scan document for available headings
scanDocumentHeadings() {
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
return Array.from(headings)
.map(h => h.textContent.trim())
.filter(text => text.length > 0 && !text.toLowerCase().includes('control'));
}
// Subscribe to debug messages
subscribe(callback) {
this.subscribers.push(callback);
return () => {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
};
}
// Notify all subscribers
notifySubscribers(message) {
this.subscribers.forEach(callback => {
try {
callback(message);
} catch (error) {
console.error('Debug subscriber error:', error);
}
});
}
// Toggle debug system
setEnabled(enabled) {
this.isEnabled = enabled;
this.addMessage(
`Debug system ${enabled ? 'enabled' : 'disabled'}`,
'INFO',
'DebugSystem',
{ eventType: 'SYSTEM' }
);
}
// Get statistics
getStats() {
const stats = {
total: this.messages.length,
byCategory: {},
bySource: {},
enabled: this.isEnabled,
criteria: { ...this.selectionCriteria }
};
this.messages.forEach(msg => {
stats.byCategory[msg.category] = (stats.byCategory[msg.category] || 0) + 1;
stats.bySource[msg.source] = (stats.bySource[msg.source] || 0) + 1;
});
return stats;
}
}
// Initialize and expose globally
window.MarkitectDebugSystem = new MarkitectDebugSystem();

View File

@@ -1,544 +0,0 @@
/**
* SectionManager Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Manages the collection of sections and their state transitions.
*
* Dependencies:
* - EditState enum (imported)
* - SectionType enum (imported)
* - Section class (imported)
* - debug function (imported)
*/
// Import dependencies - these will be separate modules
const EditState = Object.freeze({
ORIGINAL: 'original',
EDITING: 'editing',
MODIFIED: 'modified',
SAVED: 'saved'
});
const SectionType = Object.freeze({
HEADING: 'heading',
PARAGRAPH: 'paragraph',
LIST: 'list',
CODE: 'code',
QUOTE: 'quote',
TABLE: 'table',
HR: 'hr',
IMAGE: 'image'
});
// Debug function (will be extracted to utils)
function debug(message, category = 'INFO') {
// Simple console debug for now - will be enhanced later
console.log(`DEBUG ${category}: ${message}`);
}
/**
* Section Class - manages individual section state and content
*/
class Section {
constructor(id, markdown, type) {
this.id = id;
this.originalMarkdown = markdown;
this.currentMarkdown = markdown;
this.editingMarkdown = markdown;
this.pendingMarkdown = null;
this.type = type;
this.state = EditState.ORIGINAL;
this.domElement = null;
this.lastSaved = null;
this.created = new Date();
}
static generateId(markdown, position, strategy = 'hash', parentId = null) {
return this.generateIdWithStrategy(markdown, position, strategy, parentId);
}
static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
const sanitizedContent = this.sanitizeContentForId(markdown);
const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
const sectionType = this.detectType(markdown);
switch (strategy) {
case 'timestamp':
return this.generateTimestampId(normalizedContent, position, sectionType);
case 'sequential':
return this.generateSequentialId(normalizedContent, position, sectionType);
case 'hierarchical':
return this.generateHierarchicalId(normalizedContent, position, parentId);
case 'hash':
default:
return this.generateAdvancedId(normalizedContent, position, sectionType);
}
}
static generateAdvancedId(content, position, sectionType) {
const contentHash = this.generateCryptoHash(content);
const safeType = sectionType || 'paragraph';
const typePrefix = safeType.substring(0, 3);
const positionHex = position.toString(16).padStart(2, '0');
return `section-${typePrefix}-${contentHash}-${positionHex}`;
}
static generateCryptoHash(content) {
let hash = 0;
if (content.length === 0) return '00000000';
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
return hexHash.substring(0, 8);
}
static normalizeContentForHashing(content) {
if (!content || typeof content !== 'string') {
return '';
}
return content
.trim()
.replace(/\s+/g, ' ')
.replace(/\r\n/g, '\n')
.toLowerCase();
}
static sanitizeContentForId(content) {
if (!content || typeof content !== 'string') {
return '';
}
return content
.replace(/<[^>]*>/g, '')
.replace(/javascript:/gi, '')
.replace(/[^\w\s\-_.#]/g, '')
.trim();
}
static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
const timestamp = Date.now().toString(36);
const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
const safeType = sectionType || 'paragraph';
const typePrefix = safeType.substring(0, 3);
return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
}
static generateSequentialId(content, position, sectionType = 'paragraph') {
const safeType = sectionType || 'paragraph';
const typePrefix = safeType.substring(0, 3);
const seqNumber = (position || 0).toString().padStart(3, '0');
const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
}
static generateHierarchicalId(content, position, parentId = null) {
const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
if (parentId) {
const childIndex = (position || 0).toString().padStart(2, '0');
return `${parentId}-child-${childIndex}-${contentHash}`;
} else {
return `section-root-${position || 0}-${contentHash}`;
}
}
static detectType(markdown) {
if (!markdown || typeof markdown !== 'string') {
return SectionType.PARAGRAPH;
}
const content = markdown.replace(/^\n+|\n+$/g, '');
if (!content) {
return SectionType.PARAGRAPH;
}
const trimmed = content.trim();
// Detection order matters - most specific first
if (this.isHeading(trimmed)) {
return SectionType.HEADING;
}
if (this.isImage(trimmed)) {
return SectionType.IMAGE;
}
if (this.isCodeBlock(trimmed)) {
return SectionType.CODE;
}
return SectionType.PARAGRAPH;
}
static isHeading(trimmed) {
const headingPattern = /^#{1,6}\s+.+/;
return headingPattern.test(trimmed);
}
static isImage(trimmed) {
const imagePattern = /!\[.*?\]\([^)]+\)/;
return imagePattern.test(trimmed);
}
static isCodeBlock(trimmed) {
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
return true;
}
if (trimmed.includes('```') || trimmed.includes('~~~')) {
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
if (codeBlockPattern.test(trimmed)) {
return true;
}
}
return false;
}
startEdit() {
if (this.state === EditState.EDITING) {
throw new Error(`Section ${this.id} is already being edited`);
}
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
this.state = EditState.EDITING;
return this.editingMarkdown;
}
updateContent(markdown) {
if (this.state !== EditState.EDITING) {
throw new Error(`Section ${this.id} is not in editing state`);
}
this.editingMarkdown = markdown;
}
acceptChanges() {
if (this.state !== EditState.EDITING) {
throw new Error(`Section ${this.id} is not in editing state`);
}
this.currentMarkdown = this.editingMarkdown;
this.editingMarkdown = null;
this.pendingMarkdown = null;
this.state = EditState.SAVED;
this.lastSaved = new Date();
return this.currentMarkdown;
}
cancelChanges() {
if (this.state !== EditState.EDITING) {
throw new Error(`Section ${this.id} is not in editing state`);
}
this.editingMarkdown = null;
if (this.pendingMarkdown !== null) {
this.state = EditState.MODIFIED;
return this.pendingMarkdown;
} else if (this.lastSaved !== null) {
this.state = EditState.SAVED;
return this.currentMarkdown;
} else {
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
return this.currentMarkdown;
}
}
stopEditing() {
if (this.state !== EditState.EDITING) {
return this.state;
}
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
this.pendingMarkdown = this.editingMarkdown;
this.state = EditState.MODIFIED;
} else {
this.pendingMarkdown = null;
if (this.lastSaved !== null) {
this.state = EditState.SAVED;
} else {
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
}
}
this.editingMarkdown = null;
return this.state;
}
resetToOriginal() {
this.currentMarkdown = this.originalMarkdown;
this.editingMarkdown = this.originalMarkdown;
this.pendingMarkdown = null;
this.state = EditState.ORIGINAL;
return this.originalMarkdown;
}
isEditing() {
return this.state === EditState.EDITING;
}
hasChanges() {
return this.currentMarkdown !== this.originalMarkdown;
}
getStatus() {
return {
id: this.id,
state: this.state,
hasChanges: this.hasChanges(),
isEditing: this.isEditing(),
contentLength: this.currentMarkdown.length,
lastSaved: this.lastSaved,
type: this.type,
originalLength: this.originalMarkdown.length,
currentLength: this.currentMarkdown.length
};
}
isImage() {
return this.type === SectionType.IMAGE;
}
redetectType(content = null) {
const markdown = content || this.currentMarkdown;
const oldType = this.type;
this.type = Section.detectType(markdown);
if (oldType !== this.type) {
debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
}
return this.type;
}
}
/**
* SectionManager - Manages the collection of sections
*/
class SectionManager {
constructor() {
this.sections = new Map();
this.listeners = new Map();
this.statusInterval = null;
this.lastStatusUpdate = new Date().toISOString();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => callback(data));
}
}
createSectionsFromMarkdown(markdownContent) {
// Split content into blocks separated by double newlines
const blocks = markdownContent.split(/\n\s*\n/);
const sections = [];
let position = 0;
for (const block of blocks) {
const trimmedBlock = block.trim();
if (!trimmedBlock) continue;
// Check if this block should be split further
const lines = trimmedBlock.split('\n');
let currentSection = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\s/.test(line.trim());
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
// Each heading or image starts a new section
if ((isHeading || isImage) && currentSection.trim()) {
// Save the previous section
const sectionId = Section.generateId(currentSection, position);
const sectionType = Section.detectType(currentSection);
const section = new Section(sectionId, currentSection.trim(), sectionType);
sections.push(section);
this.sections.set(sectionId, section);
position++;
currentSection = line;
} else {
if (currentSection) currentSection += '\n';
currentSection += line;
}
}
// Save the final section from this block
if (currentSection.trim()) {
const sectionId = Section.generateId(currentSection, position);
const sectionType = Section.detectType(currentSection);
const section = new Section(sectionId, currentSection.trim(), sectionType);
sections.push(section);
this.sections.set(sectionId, section);
position++;
}
}
this.emit('sections-created', { sections, count: sections.length });
return sections;
}
startEditing(sectionId) {
debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
if (section.isEditing()) {
debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
return section.editingMarkdown;
}
debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
const content = section.startEdit();
debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
return content;
}
updateContent(sectionId, markdown) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const oldType = section.type;
section.updateContent(markdown);
const newType = section.redetectType(markdown);
const eventData = {
sectionId,
markdown,
section: section.getStatus(),
typeChanged: oldType !== newType,
oldType,
newType
};
this.emit('content-updated', eventData);
if (oldType !== newType) {
this.emit('section-type-changed', {
sectionId,
oldType,
newType,
section: section.getStatus()
});
}
}
acceptChanges(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const content = section.acceptChanges();
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
return content;
}
cancelChanges(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const content = section.cancelChanges();
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
return content;
}
resetSection(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const content = section.resetToOriginal();
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
return content;
}
getDocumentMarkdown() {
const sortedSections = Array.from(this.sections.values())
.sort((a, b) => a.created - b.created);
return sortedSections.map(section => section.currentMarkdown).join('\n\n');
}
getAllSections() {
return Array.from(this.sections.values());
}
getDocumentStatus() {
const sections = Array.from(this.sections.values());
const editingSections = sections.filter(section => section.isEditing).length;
return {
totalSections: sections.length,
editingSections: editingSections
};
}
extractHeadings(content) {
if (!content) return [];
const lines = content.split('\n');
return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
}
handleSectionSplit(sectionId, newContent) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
// Remove the original section
this.sections.delete(sectionId);
// Create new sections from the content
const newSections = this.createSectionsFromMarkdown(newContent);
// Emit section-split event
this.emit('section-split', {
originalSectionId: sectionId,
newSections: newSections,
count: newSections.length
});
return newSections;
}
createSectionsFromContent(content) {
return this.createSectionsFromMarkdown(content);
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { SectionManager, Section, EditState, SectionType };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.SectionManager = SectionManager;
window.Section = Section;
window.EditState = EditState;
window.SectionType = SectionType;
}

View File

@@ -1,227 +0,0 @@
/**
* Main Markitect JavaScript Entry Point - Clean Architecture Version
*
* Uses ONLY the JSON configuration interface - NO Python-generated JavaScript!
* Initializes all controls and systems when document is ready
* Implements graceful degradation for missing dependencies
*/
// Main application module
const MarkitectMain = {
initialized: false,
config: null,
// Initialize the complete application
initialize: function() {
if (this.initialized) {
console.log('⚠️ MarkitectMain already initialized, skipping');
return;
}
console.log('🚀 MarkitectMain initializing...');
try {
// Get configuration - if not loaded, use defaults
this.config = window.markitectConfig;
if (!this.config || !this.config.loaded) {
console.warn('⚠️ Configuration not loaded, proceeding with defaults');
this.config = {
markdownContent: document.querySelector('#markdown-content')?.textContent || '',
mode: 'edit',
theme: 'github'
};
}
// Initialize core systems
this.initializeCoreComponents();
this.initializeControlPanels();
this.setupEventHandlers();
this.renderContent();
this.initialized = true;
console.log('✅ MarkitectMain initialization complete');
} catch (error) {
console.error('❌ MarkitectMain initialization failed:', error);
this.fallbackMode();
}
},
// Initialize core modular components
initializeCoreComponents: function() {
console.log('🔧 Initializing core components...');
const container = document.getElementById('markdown-content') || document.body;
// Initialize section manager
if (typeof SectionManager !== 'undefined') {
this.sectionManager = new SectionManager();
console.log('✅ SectionManager initialized');
} else {
throw new Error('SectionManager not available');
}
// Initialize DOM renderer
if (typeof DOMRenderer !== 'undefined') {
this.domRenderer = new DOMRenderer(this.sectionManager, container);
console.log('✅ DOMRenderer initialized');
} else {
throw new Error('DOMRenderer not available');
}
// Initialize debug panel
if (typeof DebugPanel !== 'undefined') {
this.debugPanel = new DebugPanel();
console.log('✅ DebugPanel initialized');
}
// Legacy DocumentControls removed - functionality now in enhanced control panels
},
// Initialize enhanced control panels with compass positioning
initializeControlPanels: function() {
console.log('🎛️ Initializing enhanced control panels with compass positioning...');
// ContentsControl (West)
if (typeof ContentsControl !== 'undefined') {
this.contentsControl = new ContentsControl();
this.contentsControl.config.position = 'w';
this.contentsControl.show();
window.contentsControl = this.contentsControl;
console.log('✅ ContentsControl initialized (West) with enhanced ControlBase');
}
// StatusControl (East)
if (typeof StatusControl !== 'undefined') {
this.statusControl = new StatusControl();
this.statusControl.config.position = 'e';
this.statusControl.show();
window.statusControl = this.statusControl;
console.log('✅ StatusControl initialized (East) with enhanced ControlBase');
}
// DebugControl (Southeast)
if (typeof DebugControl !== 'undefined') {
this.debugControl = new DebugControl();
this.debugControl.config.position = 'se';
this.debugControl.show();
window.debugControl = this.debugControl;
console.log('✅ DebugControl initialized (Southeast) with enhanced ControlBase');
}
// EditControl (Northeast)
if (typeof EditControl !== 'undefined') {
this.editControl = new EditControl();
this.editControl.config.position = 'ne';
this.editControl.show();
window.editControl = this.editControl;
console.log('✅ EditControl initialized (Northeast) with enhanced ControlBase');
}
},
// Setup core event handlers (enhanced control panels handle their own events)
setupEventHandlers: function() {
console.log('🔌 Setting up core event handlers...');
// Setup section manager event handlers for debug panel
if (this.sectionManager && this.debugPanel) {
this.sectionManager.on('sections-created', (data) => {
this.debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
});
this.sectionManager.on('edit-started', (data) => {
this.debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
});
this.sectionManager.on('changes-accepted', (data) => {
this.debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
this.updateSectionDOM(data.sectionId);
});
this.sectionManager.on('changes-cancelled', (data) => {
this.debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
});
}
// Make core components available globally for enhanced controls
window.sectionManager = this.sectionManager;
window.domRenderer = this.domRenderer;
window.debugPanel = this.debugPanel;
console.log('✅ Core event handlers and global references set up');
},
// Render content using the configuration
renderContent: function() {
console.log('📄 Rendering markdown content...');
const markdownToRender = this.config.markdownContent || '';
if (markdownToRender.trim()) {
const sections = this.sectionManager.createSectionsFromMarkdown(markdownToRender);
this.domRenderer.renderAllSections(sections);
if (this.debugPanel) {
this.debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
}
console.log(`✅ Rendered ${sections.length} sections`);
} else {
if (this.debugPanel) {
this.debugPanel.addMessage('No markdown content to initialize', 'WARNING');
}
console.warn('⚠️ No markdown content to render');
}
},
// Update section DOM after changes
updateSectionDOM: function(sectionId) {
try {
const section = this.sectionManager.sections.get(sectionId);
if (section) {
const sectionElement = this.domRenderer.findSectionElement(sectionId);
if (sectionElement) {
const newElement = this.domRenderer.renderSection(section);
sectionElement.parentNode.replaceChild(newElement, sectionElement);
if (this.debugPanel) {
this.debugPanel.addMessage(`DOM updated for section: ${sectionId}`, 'INFO');
}
}
}
} catch (error) {
console.error('❌ Failed to update section DOM:', error);
}
},
// Fallback mode if initialization fails
fallbackMode: function() {
console.warn('⚠️ Running in fallback mode');
// Basic content rendering fallback
const contentDiv = document.getElementById('markdown-content');
if (contentDiv && this.config && this.config.markdownContent) {
const basicHtml = this.config.markdownContent
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>');
contentDiv.innerHTML = `<p>${basicHtml}</p>`;
console.log('✅ Fallback content rendered');
}
}
};
// Make components globally available for debugging
window.MarkitectMain = MarkitectMain;
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
// Small delay to ensure config is loaded
setTimeout(() => MarkitectMain.initialize(), 100);
});
} else {
// DOM already ready
setTimeout(() => MarkitectMain.initialize(), 100);
}

View File

@@ -1,201 +0,0 @@
/**
* Main Markitect JavaScript Entry Point
* Initializes all controls and systems when document is ready
* Implements graceful degradation for missing dependencies
* Supports Fail Fast strict mode for development
*/
// Development mode detection
const MARKITECT_STRICT_MODE = (
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.search.includes('strict=true') ||
window.markitectStrictMode === true
);
// Utility functions for safe initialization
const MarkitectMain = {
// Safe dependency checking with timeout
checkDependencies: function() {
const dependencies = {
debugSystem: !!window.MarkitectDebugSystem,
control: !!window.Control,
statusControl: !!window.StatusControl,
debugControl: !!window.DebugControl,
contentsControl: !!window.ContentsControl,
editControl: !!window.EditControl
};
console.log('📋 Dependency check results:', dependencies);
return dependencies;
},
// Safe logging that works even without debug system
safeLog: function(message, level = 'INFO', component = 'Main', data = {}) {
console.log(`[${level}] ${component}: ${message}`);
// In strict mode, throw on errors for immediate development feedback
if (MARKITECT_STRICT_MODE && level === 'ERROR') {
console.error(`🚨 STRICT MODE: Throwing error for immediate diagnosis`);
throw new Error(`${component}: ${message}`);
}
// Try to use debug system if available
if (window.MarkitectDebugSystem && window.MarkitectDebugSystem.addMessage) {
try {
window.MarkitectDebugSystem.addMessage(message, level, component, { ...data, eventType: 'SYSTEM' });
} catch (error) {
console.warn('Debug system logging failed:', error);
if (MARKITECT_STRICT_MODE) {
throw error; // Fail fast in development
}
}
}
},
// Safe control initialization with fallbacks
initializeControl: function(controlClass, controlName, icon = '🔧') {
const timeout = setTimeout(() => {
const message = `${controlName} initialization timed out`;
console.warn(message);
if (MARKITECT_STRICT_MODE) {
throw new Error(message); // Fail fast in development
}
}, 5000);
try {
if (!controlClass) {
const message = `${controlName} class not available, skipping`;
this.safeLog(message, MARKITECT_STRICT_MODE ? 'ERROR' : 'WARNING');
clearTimeout(timeout);
return null;
}
const controlInstance = new controlClass();
if (!controlInstance || typeof controlInstance.createControl !== 'function') {
throw new Error(`Invalid ${controlName} instance`);
}
const element = controlInstance.createControl();
if (!element) {
throw new Error(`${controlName} failed to create element`);
}
clearTimeout(timeout);
this.safeLog(`${controlName} initialized successfully`, 'SUCCESS');
return controlInstance;
} catch (error) {
clearTimeout(timeout);
this.safeLog(`${controlName} initialization failed: ${error.message}`, 'ERROR');
// Create minimal fallback control if core Control class exists
if (window.Control && controlName === 'StatusControl') {
return this.createFallbackControl(controlName, icon);
}
return null;
}
},
// Create minimal fallback control for essential controls
createFallbackControl: function(name, icon) {
try {
const fallback = Object.create(window.Control);
fallback.config = {
icon: icon,
title: `${name} (Fallback)`,
className: `${name.toLowerCase()}-fallback`,
defaultContent: `${name} is running in fallback mode due to initialization issues.`,
ariaLabel: `${name} Fallback Control`,
position: 'e'
};
const element = fallback.createControl();
if (element) {
this.safeLog(`${name} fallback control created`, 'INFO');
return { control: fallback };
}
} catch (error) {
this.safeLog(`Fallback control creation failed: ${error.message}`, 'ERROR');
}
return null;
},
// Main initialization with comprehensive error handling
initialize: function() {
this.safeLog('🚀 Initializing Markitect controls and systems...', 'INFO');
// Check dependencies first
const deps = this.checkDependencies();
if (!deps.control) {
this.safeLog('❌ Core Control system not available, cannot initialize UI controls', 'ERROR');
return;
}
const initializedControls = {};
let successCount = 0;
let totalAttempts = 0;
// Initialize controls with graceful degradation
const controlsToInit = [
{ class: window.StatusControl, name: 'StatusControl', key: 'statusControl', icon: '📊', essential: true },
{ class: window.DebugControl, name: 'DebugControl', key: 'debugControl', icon: '🪲', essential: false },
{ class: window.ContentsControl, name: 'ContentsControl', key: 'contentsControl', icon: '☰', essential: false },
{ class: window.EditControl, name: 'EditControl', key: 'editControl', icon: '✏️', essential: false }
];
controlsToInit.forEach(({ class: controlClass, name, key, icon, essential }) => {
totalAttempts++;
const instance = this.initializeControl(controlClass, name, icon);
if (instance) {
initializedControls[key] = instance.control || instance;
window[key] = initializedControls[key];
successCount++;
} else if (essential) {
this.safeLog(`Essential control ${name} failed to initialize`, 'ERROR');
}
});
// Report initialization results
const successRate = Math.round((successCount / totalAttempts) * 100);
if (successCount === totalAttempts) {
this.safeLog('✅ All controls initialized successfully', 'SUCCESS');
} else if (successCount > 0) {
this.safeLog(`⚠️ Partial initialization: ${successCount}/${totalAttempts} controls (${successRate}%) initialized`, 'WARNING');
} else {
this.safeLog('❌ No controls could be initialized', 'ERROR');
}
// Set up global error handlers for runtime protection
this.setupErrorHandlers();
this.safeLog(`✅ Markitect initialization complete (${successCount}/${totalAttempts} controls active)`, 'INFO');
},
// Set up global error handlers
setupErrorHandlers: function() {
// Catch unhandled errors
window.addEventListener('error', (event) => {
this.safeLog(`Unhandled error: ${event.message} at ${event.filename}:${event.lineno}`, 'ERROR');
});
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.safeLog(`Unhandled promise rejection: ${event.reason}`, 'ERROR');
event.preventDefault(); // Prevent console spam
});
}
};
// Initialize when DOM is ready with additional safety
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => MarkitectMain.initialize(), 100); // Brief delay for dependencies
});
} else {
// DOM already loaded
setTimeout(() => MarkitectMain.initialize(), 100);
}

View File

@@ -1,207 +0,0 @@
/**
* DocumentNavigator Plugin Definition
*
* Plugin definition for the Substack-style document navigation widget.
* Provides floating table of contents with smooth scrolling and scroll spy.
*/
export default {
name: 'DocumentNavigator',
version: '1.0.0',
description: 'Substack-style floating document navigation with table of contents',
author: 'Markitect Core',
category: 'navigation',
// Dependencies that must be loaded first
dependencies: ['UIWidget'],
// Mixins to apply (none required for this widget)
mixins: [],
// Lazy load the actual widget class
async load() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
return DocumentNavigator;
},
// Default configuration
defaultOptions: {
position: 'left', // 'left' or 'right' side
collapsed: true, // Start in collapsed state
autoHide: true, // Hide on mobile devices
maxHeadingLevel: 3, // Include H1, H2, H3
enableScrollSpy: true, // Highlight current section
smoothScroll: true, // Smooth scroll to headings
animationDuration: 300, // Animation timing in ms
minHeadings: 2, // Minimum headings to show widget
theme: 'default', // Theme variant
// Layout options
width: '280px', // Expanded width
collapsedWidth: '40px', // Collapsed width
offset: { // Position offset
top: '80px',
side: '20px'
},
// Accessibility
enableKeyboard: true, // Keyboard navigation support
ariaLabel: 'Document Navigation'
},
// Plugin lifecycle hooks
async onLoad(instance, options) {
console.log('DocumentNavigator plugin loaded:', {
headings: instance.headings.length,
position: options.position,
collapsed: options.collapsed
});
// Auto-initialize after load
await instance.initialize();
return instance;
},
async onUnload(instance) {
console.log('DocumentNavigator plugin unloading');
await instance.destroy();
},
// Feature flags and capabilities
capabilities: {
draggable: false, // Not draggable (fixed position)
resizable: false, // Not resizable (fixed width)
themeable: true, // Supports themes
persistent: false, // Rebuilds on page changes
responsive: true, // Responsive behavior
keyboard: true, // Keyboard accessible
scrollSpy: true, // Scroll spy functionality
smoothScroll: true // Smooth scroll navigation
},
// Integration requirements
requirements: {
container: true, // Requires container element
headings: true, // Requires document headings
scrollable: true // Requires scrollable content
},
// Event types emitted by this widget
events: [
'rendered', // Widget rendered to DOM
'navigate', // User navigated to heading
'toggle', // Widget expanded/collapsed
'theme-changed', // Theme was changed
'destroyed' // Widget was destroyed
],
// CSS classes used by this widget
cssClasses: [
'document-navigator', // Main widget class
'navigator-toggle', // Toggle button
'navigator-list', // Navigation list
'navigator-item', // Navigation items
'navigator-link', // Navigation links
'navigator-header', // List header
'navigator-close', // Close button
'navigator-empty' // Empty state
],
// Theme variants
themes: {
default: {
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#e1e5e9',
textColor: '#333',
activeColor: '#1976d2',
activeBackground: '#e3f2fd'
},
dark: {
backgroundColor: 'rgba(45, 45, 45, 0.95)',
borderColor: '#555',
textColor: '#e0e0e0',
activeColor: '#64b5f6',
activeBackground: '#1e3a8a'
},
minimal: {
backgroundColor: 'rgba(248, 249, 250, 0.90)',
borderColor: '#dee2e6',
textColor: '#495057',
activeColor: '#007bff',
activeBackground: '#e7f1ff'
}
},
// Usage examples
examples: {
basic: {
description: 'Basic document navigator on the left side',
code: `
const navigator = await widgetSystem.createWidget('DocumentNavigator');
await navigator.show();
`
},
customized: {
description: 'Customized navigator with specific options',
code: `
const navigator = await widgetSystem.createWidget('DocumentNavigator', {
position: 'right',
collapsed: false,
maxHeadingLevel: 4,
theme: 'dark'
});
await navigator.show();
`
},
withContainer: {
description: 'Navigator for specific container content',
code: `
const container = document.getElementById('article-content');
const navigator = await widgetSystem.createWidget('DocumentNavigator', {
container: container,
minHeadings: 1
});
await navigator.show();
`
}
},
// Development and testing helpers
dev: {
testHeadingStructure() {
// Helper to create test content with headings
const testContent = `
<h1>Chapter 1: Introduction</h1>
<p>Lorem ipsum content...</p>
<h2>Section 1.1: Overview</h2>
<h3>Subsection 1.1.1: Details</h3>
<h2>Section 1.2: Implementation</h2>
<h1>Chapter 2: Advanced Topics</h1>
<h2>Section 2.1: Performance</h2>
`;
const container = document.createElement('div');
container.innerHTML = testContent;
container.style.cssText = 'height: 2000px; padding: 2rem;';
document.body.appendChild(container);
return container;
},
async createTestInstance(options = {}) {
// Helper to create test instance with sample content
const container = this.testHeadingStructure();
const navigator = new (await this.load())({
container,
collapsed: false,
...options
});
await navigator.initialize();
await navigator.render();
return { navigator, container };
}
}
};

View File

@@ -1,216 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test Runner for JavaScript Refactoring
*
* Drives component extraction and testing during architecture refactoring.
* Ensures all functionality remains stable while achieving separation of concerns.
*/
class RefactorTestRunner {
constructor() {
this.tests = [];
this.passed = 0;
this.failed = 0;
this.currentSuite = null;
this.setupDOM();
}
setupDOM() {
// Set up minimal DOM environment for testing
if (typeof document === 'undefined') {
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'http://localhost',
pretendToBeVisual: true,
resources: 'usable'
});
global.window = dom.window;
global.document = dom.window.document;
global.HTMLElement = dom.window.HTMLElement;
global.Event = dom.window.Event;
global.CustomEvent = dom.window.CustomEvent;
// Only set navigator if it doesn't exist
if (typeof global.navigator === 'undefined') {
global.navigator = dom.window.navigator;
}
}
}
describe(suiteName, fn) {
console.log(`\n📁 ${suiteName}`);
this.currentSuite = suiteName;
fn();
this.currentSuite = null;
}
it(testName, fn) {
const fullName = this.currentSuite ? `${this.currentSuite}: ${testName}` : testName;
try {
fn();
console.log(`${testName}`);
this.passed++;
} catch (error) {
console.log(`${testName}`);
console.log(` Error: ${error.message}`);
if (error.stack) {
console.log(` Stack: ${error.stack.split('\n')[1]?.trim()}`);
}
this.failed++;
}
}
expect(actual) {
return {
toBe: (expected) => {
if (actual !== expected) {
throw new Error(`Expected ${expected}, got ${actual}`);
}
},
toBeTruthy: () => {
if (!actual) {
throw new Error(`Expected truthy value, got ${actual}`);
}
},
toBeFalsy: () => {
if (actual) {
throw new Error(`Expected falsy value, got ${actual}`);
}
},
toEqual: (expected) => {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
},
toContain: (expected) => {
if (!actual.includes(expected)) {
throw new Error(`Expected ${actual} to contain ${expected}`);
}
},
toHaveProperty: (property) => {
if (!(property in actual)) {
throw new Error(`Expected object to have property ${property}`);
}
},
toBeInstanceOf: (expectedClass) => {
if (!(actual instanceof expectedClass)) {
throw new Error(`Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`);
}
}
};
}
/**
* Test that a component can be extracted from the monolith without breaking functionality
*/
testComponentExtraction(componentName, extractFn, originalTests) {
this.describe(`Component Extraction: ${componentName}`, () => {
this.it('should extract without syntax errors', () => {
try {
const component = extractFn();
this.expect(component).toBeTruthy();
} catch (error) {
throw new Error(`Component extraction failed: ${error.message}`);
}
});
this.it('should maintain original API', () => {
const component = extractFn();
originalTests.forEach(test => {
try {
test(component);
} catch (error) {
throw new Error(`API compatibility test failed: ${error.message}`);
}
});
});
});
}
/**
* Test component integration after extraction
*/
testComponentIntegration(components, integrationTests) {
this.describe('Component Integration', () => {
integrationTests.forEach((test, index) => {
this.it(`integration test ${index + 1}`, () => {
test(components);
});
});
});
}
/**
* Setup test environment with mock dependencies
*/
setupTestEnvironment() {
// Create test container
const container = document.createElement('div');
container.id = 'test-container';
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
// Mock any global dependencies
global.mockSectionManager = {
sections: new Map(),
createSectionsFromMarkdown: () => [],
startEditing: () => true,
stopEditing: () => true,
getAllSections: () => []
};
return { container };
}
/**
* Cleanup test environment
*/
cleanupTestEnvironment() {
const container = document.getElementById('test-container');
if (container) {
container.remove();
}
// Clear any global mocks
delete global.mockSectionManager;
}
async run() {
console.log('🧪 TDD Refactoring Test Runner Starting...\n');
const startTime = Date.now();
// Run all collected tests
// Tests will be added by importing component test files
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`\n📊 Test Results:`);
console.log(` ✅ Passed: ${this.passed}`);
console.log(` ❌ Failed: ${this.failed}`);
console.log(` ⏱️ Duration: ${duration}ms`);
if (this.failed > 0) {
console.log(`\n${this.failed} test(s) failed. Refactoring should not proceed.`);
process.exit(1);
} else {
console.log(`\n✅ All tests passed! Refactoring is safe to continue.`);
}
}
}
// Export for use in component tests
if (typeof module !== 'undefined' && module.exports) {
module.exports = { RefactorTestRunner };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.RefactorTestRunner = RefactorTestRunner;
}
module.exports = RefactorTestRunner;

View File

@@ -1,521 +0,0 @@
#!/usr/bin/env node
/**
* Comprehensive Component Integration Test
*
* Tests that extracted components work together properly.
* Verifies the complete workflow: Section Creation → Rendering → Editing → Saving
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
runner.describe('Component Integration Tests', () => {
runner.it('should load all extracted components', () => {
try {
// Load extracted components
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
runner.expect(sectionModule.SectionManager).toBeTruthy();
runner.expect(sectionModule.Section).toBeTruthy();
runner.expect(domModule.DOMRenderer).toBeTruthy();
runner.expect(domModule.FloatingMenu).toBeTruthy();
// Set globals for other tests
global.ExtractedSectionManager = sectionModule.SectionManager;
global.ExtractedSection = sectionModule.Section;
global.ExtractedDOMRenderer = domModule.DOMRenderer;
global.ExtractedFloatingMenu = domModule.FloatingMenu;
global.ExtractedEditState = sectionModule.EditState;
} catch (error) {
throw new Error(`Failed to load extracted components: ${error.message}`);
}
});
runner.it('should support complete section creation workflow', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Test workflow: Create sections from markdown
const testMarkdown = `# Main Heading
This is the introduction content.
## Subheading One
Content for first subsection.
![Test Image](https://example.com/image.jpg)
## Subheading Two
Content for second subsection.`;
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
// Verify sections were created
// Expected: heading+paragraph, heading+paragraph, image, heading+paragraph = 4 sections
runner.expect(sections.length).toBe(4);
runner.expect(sections[0].type).toBe('heading');
runner.expect(sections[2].type).toBe('image');
// Verify DOM rendering
domRenderer.renderAllSections(sections);
const renderedElements = container.querySelectorAll('.ui-edit-section');
runner.expect(renderedElements.length).toBe(sections.length);
// Cleanup
document.body.removeChild(container);
});
runner.it('should support complete editing workflow', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const EditState = global.ExtractedEditState;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Create and render sections
const testMarkdown = '# Test Heading\nOriginal content here.';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const sectionId = sections[0].id;
const section = sectionManager.sections.get(sectionId);
// Test workflow: Start editing
runner.expect(section.state).toBe(EditState.ORIGINAL);
runner.expect(section.isEditing()).toBeFalsy();
const content = sectionManager.startEditing(sectionId);
runner.expect(content).toContain('Test Heading');
runner.expect(section.isEditing()).toBeTruthy();
runner.expect(section.state).toBe(EditState.EDITING);
// Test workflow: Update content
const newContent = '# Updated Heading\nModified content here.';
sectionManager.updateContent(sectionId, newContent);
runner.expect(section.editingMarkdown).toBe(newContent);
// Test workflow: Accept changes
sectionManager.acceptChanges(sectionId);
runner.expect(section.currentMarkdown).toBe(newContent);
runner.expect(section.state).toBe(EditState.SAVED);
runner.expect(section.isEditing()).toBeFalsy();
// Cleanup
document.body.removeChild(container);
});
runner.it('should support accept/cancel button functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Create and render sections
const testMarkdown = '# Test Heading\nOriginal content here.';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const sectionId = sections[0].id;
const section = sectionManager.sections.get(sectionId);
// Start editing to trigger floating menu with buttons
sectionManager.startEditing(sectionId);
// Check if floating menu exists
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
// Find buttons in the floating menu
const menuElement = domRenderer.currentFloatingMenu.element;
runner.expect(menuElement).toBeTruthy();
const buttons = menuElement.querySelectorAll('button');
runner.expect(buttons.length >= 2).toBeTruthy(); // At least Accept and Cancel buttons
const acceptBtn = Array.from(buttons).find(btn => btn.textContent === 'Accept');
const cancelBtn = Array.from(buttons).find(btn => btn.textContent === 'Cancel');
runner.expect(acceptBtn).toBeTruthy();
runner.expect(cancelBtn).toBeTruthy();
// Test Accept button functionality
runner.expect(section.isEditing()).toBeTruthy();
// Simulate updating content and clicking Accept
const textarea = menuElement.querySelector('textarea');
runner.expect(textarea).toBeTruthy();
textarea.value = '# Updated Heading\nUpdated content via button.';
acceptBtn.click();
// After clicking Accept, section should be saved and menu hidden
runner.expect(section.isEditing()).toBeFalsy();
runner.expect(section.currentMarkdown).toContain('Updated Heading');
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
// Cleanup
document.body.removeChild(container);
});
runner.it('should support cancel button functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Create and render sections
const testMarkdown = '# Original Heading\nOriginal content here.';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const sectionId = sections[0].id;
const section = sectionManager.sections.get(sectionId);
// Start editing
sectionManager.startEditing(sectionId);
// Find buttons in the floating menu
const menuElement = domRenderer.currentFloatingMenu.element;
const cancelBtn = Array.from(menuElement.querySelectorAll('button')).find(btn => btn.textContent === 'Cancel');
runner.expect(cancelBtn).toBeTruthy();
runner.expect(section.isEditing()).toBeTruthy();
// Simulate changing content but then canceling
const textarea = menuElement.querySelector('textarea');
textarea.value = '# Changed Heading\nThis should be discarded.';
cancelBtn.click();
// After clicking Cancel, section should not be saved and menu hidden
runner.expect(section.isEditing()).toBeFalsy();
runner.expect(section.currentMarkdown).toContain('Original Heading'); // Original content preserved
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
// Cleanup
document.body.removeChild(container);
});
runner.it('should support event-driven communication', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Track events
let sectionsCreatedEvent = null;
let editStartedEvent = null;
sectionManager.on('sections-created', (data) => {
sectionsCreatedEvent = data;
});
sectionManager.on('edit-started', (data) => {
editStartedEvent = data;
});
// Test event: sections-created
const testMarkdown = '# Test\nContent';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
runner.expect(sectionsCreatedEvent).toBeTruthy();
runner.expect(sectionsCreatedEvent.sections).toEqual(sections);
runner.expect(sectionsCreatedEvent.count).toBe(1);
// Test event: edit-started
const sectionId = sections[0].id;
sectionManager.startEditing(sectionId);
runner.expect(editStartedEvent).toBeTruthy();
runner.expect(editStartedEvent.sectionId).toBe(sectionId);
runner.expect(editStartedEvent.content).toContain('Test');
// Cleanup
document.body.removeChild(container);
});
runner.it('should support section type detection and rendering', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const Section = global.ExtractedSection;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Test different section types
const testMarkdown = `# Heading Section
Regular paragraph content.
![Image Section](https://example.com/test.jpg)
\`\`\`javascript
// Code section
console.log('test');
\`\`\``;
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
// Verify type detection - adjusted for actual parsing behavior
// Expected: heading+paragraph, image, code = 3 sections
runner.expect(sections[0].type).toBe('heading'); // Combined heading+paragraph
runner.expect(sections[1].type).toBe('image'); // Image section
runner.expect(sections[2].type).toBe('code'); // Code section
// Verify image detection
runner.expect(sections[1].isImage()).toBeTruthy(); // Image is now at index 1
runner.expect(sections[0].isImage()).toBeFalsy();
// Verify rendering handles different types
domRenderer.renderAllSections(sections);
const renderedElements = container.querySelectorAll('.ui-edit-section');
runner.expect(renderedElements.length).toBe(sections.length);
// Cleanup
document.body.removeChild(container);
});
runner.it('should support FloatingMenu integration', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const FloatingMenu = global.ExtractedFloatingMenu;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Create and render sections
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const sectionId = sections[0].id;
// Test showing editor (which uses FloatingMenu)
domRenderer.showEditor(sectionId, 'test content');
// Verify floating menu state
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
runner.expect(domRenderer.currentFloatingMenu.sectionId).toBe(sectionId);
runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
// Test hiding editor
domRenderer.hideCurrentEditor();
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
runner.expect(domRenderer.editingSections.has(sectionId)).toBeFalsy();
// Cleanup
document.body.removeChild(container);
});
runner.it('should support complete click-to-edit workflow', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Create and render sections
const testMarkdown = '# Test Heading\nTest content for editing';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const sectionId = sections[0].id;
const element = domRenderer.findSectionElement(sectionId);
// Simulate click event
const clickEvent = new Event('click', { bubbles: true });
Object.defineProperty(clickEvent, 'target', { value: element });
// Test complete workflow
domRenderer.handleSectionClick(clickEvent);
// Verify editing state was triggered
const section = sectionManager.sections.get(sectionId);
runner.expect(section.isEditing()).toBeTruthy();
runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
// Cleanup
document.body.removeChild(container);
});
runner.it('should support document status tracking', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionManager = new SectionManager();
const container = document.createElement('div');
const domRenderer = new DOMRenderer(sectionManager, container);
// Test initial status
let status = sectionManager.getDocumentStatus();
runner.expect(status.totalSections).toBe(0);
runner.expect(status.editingSections).toBe(0);
// Create sections
const testMarkdown = '# Section 1\nContent 1\n\n# Section 2\nContent 2';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
status = sectionManager.getDocumentStatus();
runner.expect(status.totalSections).toBe(2);
runner.expect(status.editingSections).toBe(2); // Bug compatibility (isEditing property exists)
// Test getAllSections
const allSections = sectionManager.getAllSections();
runner.expect(allSections.length).toBe(2);
runner.expect(allSections[0].currentMarkdown).toContain('Section 1');
runner.expect(allSections[1].currentMarkdown).toContain('Section 2');
});
runner.it('should support event tracking and analytics', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const container = document.createElement('div');
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Test event tracking
domRenderer.trackEvent('test-event', { data: 'test' });
domRenderer.trackEvent('section-click', { sectionId: 'test-123' });
const stats = domRenderer.getEventStats();
runner.expect(stats.totalEvents).toBe(1); // Only section-click is tracked in stats
runner.expect(stats.stats['section-click']).toBe(1);
runner.expect(stats.recentEvents.length).toBe(2);
runner.expect(stats.recentEvents[0].type).toBe('test-event');
runner.expect(stats.recentEvents[1].type).toBe('section-click');
});
// Integration stress test
runner.it('should handle complex document with multiple operations', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
// Setup
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
// Complex document
const complexMarkdown = `# Document Title
Introduction paragraph with some content.
## Section A
Content for section A with details.
![Test Image](https://example.com/test.jpg)
### Subsection A.1
More detailed content here.
\`\`\`javascript
function test() {
console.log('code block');
}
\`\`\`
## Section B
Final section content.`;
// Create and render
const sections = sectionManager.createSectionsFromMarkdown(complexMarkdown);
domRenderer.renderAllSections(sections);
runner.expect(sections.length).toBe(6); // Adjusted based on actual parsing
// Test editing multiple sections
const firstSection = sections[0];
const imageSection = sections.find(s => s.isImage());
const codeSection = sections.find(s => s.type === 'code');
// Edit first section
sectionManager.startEditing(firstSection.id);
sectionManager.updateContent(firstSection.id, '# Updated Title\nUpdated intro.');
sectionManager.acceptChanges(firstSection.id);
// Edit image section
sectionManager.startEditing(imageSection.id);
sectionManager.updateContent(imageSection.id, '![Updated Image](https://example.com/new.jpg)');
sectionManager.acceptChanges(imageSection.id);
// Verify changes
runner.expect(firstSection.currentMarkdown).toContain('Updated Title');
runner.expect(imageSection.currentMarkdown).toContain('Updated Image');
// Verify document reconstruction
const finalMarkdown = sectionManager.getDocumentMarkdown();
runner.expect(finalMarkdown).toContain('Updated Title');
runner.expect(finalMarkdown).toContain('Updated Image');
runner.expect(finalMarkdown).toContain('Section B');
// Cleanup
document.body.removeChild(container);
});
});
module.exports = runner;
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Running Component Integration Tests');
runner.run().then(() => {
console.log('✅ Component integration tests completed');
});
}

View File

@@ -1,191 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test for Debug Panel Component Extraction
*
* Tests the extraction of DebugPanel from the monolithic editor.js
* DebugPanel handles debug message display and management.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
// Define expected DebugPanel API
const EXPECTED_DEBUGPANEL_API = [
'constructor',
'toggle',
'update',
'clear',
'addMessage',
'show',
'hide',
'getMessageCount',
'getRecentMessages'
];
runner.describe('DebugPanel Component Extraction', () => {
runner.it('should define expected API methods', () => {
const expectedMethods = EXPECTED_DEBUGPANEL_API;
runner.expect(expectedMethods.length).toBe(9);
runner.expect(expectedMethods).toContain('toggle');
runner.expect(expectedMethods).toContain('update');
runner.expect(expectedMethods).toContain('addMessage');
});
runner.it('should load extracted DebugPanel component', () => {
// Load the extracted component
delete require.cache[require.resolve('../components/debug-panel.js')];
try {
const module = require('../components/debug-panel.js');
runner.expect(module.DebugPanel).toBeTruthy();
// Set global for other tests
global.ExtractedDebugPanel = module.DebugPanel;
} catch (error) {
throw new Error(`Failed to load extracted DebugPanel: ${error.message}`);
}
});
runner.it('should preserve constructor functionality', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
runner.expect(debugPanel).toBeInstanceOf(DebugPanel);
runner.expect(debugPanel.messages).toBeInstanceOf(Array);
runner.expect(debugPanel.isActive).toBeFalsy();
});
runner.it('should preserve message handling functionality', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
// Test adding messages
debugPanel.addMessage('Test message', 'INFO');
runner.expect(debugPanel.getMessageCount()).toBe(1);
const recentMessages = debugPanel.getRecentMessages(1);
runner.expect(recentMessages.length).toBe(1);
runner.expect(recentMessages[0].message).toBe('Test message');
runner.expect(recentMessages[0].category).toBe('INFO');
});
runner.it('should preserve toggle functionality', () => {
const DebugPanel = global.ExtractedDebugPanel;
// Create container element
const container = document.createElement('div');
container.id = 'debug-messages-container';
container.style.display = 'none';
document.body.appendChild(container);
const debugButton = document.createElement('button');
debugButton.id = 'toggle-debug';
debugButton.textContent = '🔍 Debug';
document.body.appendChild(debugButton);
const debugPanel = new DebugPanel();
// Test toggle on
debugPanel.toggle();
runner.expect(debugPanel.isActive).toBeTruthy();
// Test toggle off
debugPanel.toggle();
runner.expect(debugPanel.isActive).toBeFalsy();
// Cleanup
document.body.removeChild(container);
document.body.removeChild(debugButton);
});
runner.it('should preserve update functionality', () => {
const DebugPanel = global.ExtractedDebugPanel;
const container = document.createElement('div');
container.id = 'debug-messages-container';
document.body.appendChild(container);
const debugButton = document.createElement('button');
debugButton.id = 'toggle-debug';
debugButton.textContent = '🔍 Debug';
document.body.appendChild(debugButton);
const debugPanel = new DebugPanel();
debugPanel.show();
debugPanel.addMessage('Test message 1', 'INFO');
debugPanel.addMessage('Test message 2', 'ERROR');
debugPanel.update();
runner.expect(container.innerHTML.length > 100).toBeTruthy();
runner.expect(container.innerHTML).toContain('Test message 1');
runner.expect(container.innerHTML).toContain('Test message 2');
// Cleanup
document.body.removeChild(container);
document.body.removeChild(debugButton);
});
runner.it('should preserve clear functionality', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
debugPanel.addMessage('Test message 1', 'INFO');
debugPanel.addMessage('Test message 2', 'ERROR');
runner.expect(debugPanel.getMessageCount()).toBe(2);
debugPanel.clear();
runner.expect(debugPanel.getMessageCount()).toBe(0);
});
runner.it('should have core debug panel methods', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
// Should have core methods
runner.expect(typeof debugPanel.toggle === 'function').toBeTruthy();
runner.expect(typeof debugPanel.update === 'function').toBeTruthy();
runner.expect(typeof debugPanel.addMessage === 'function').toBeTruthy();
runner.expect(typeof debugPanel.clear === 'function').toBeTruthy();
});
runner.it('should handle message categories properly', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
// Test different message categories
debugPanel.addMessage('Info message', 'INFO');
debugPanel.addMessage('Warning message', 'WARNING');
debugPanel.addMessage('Error message', 'ERROR');
debugPanel.addMessage('Success message', 'SUCCESS');
const messages = debugPanel.getRecentMessages(4);
runner.expect(messages.length).toBe(4);
const categories = messages.map(m => m.category);
runner.expect(categories).toContain('INFO');
runner.expect(categories).toContain('WARNING');
runner.expect(categories).toContain('ERROR');
runner.expect(categories).toContain('SUCCESS');
});
});
module.exports = {
runner,
EXPECTED_DEBUGPANEL_API
};
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Testing DebugPanel Component Extraction');
runner.run().then(() => {
console.log('✅ DebugPanel extraction tests completed');
});
}

View File

@@ -1,210 +0,0 @@
#!/usr/bin/env node
/**
* DebugPanel Integration Test
*
* Tests that the extracted DebugPanel component integrates properly
* with the existing SectionManager and DOMRenderer components.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
runner.describe('DebugPanel Integration Tests', () => {
runner.it('should load all extracted components including DebugPanel', () => {
try {
// Load extracted components
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
const debugModule = require('../components/debug-panel.js');
runner.expect(sectionModule.SectionManager).toBeTruthy();
runner.expect(domModule.DOMRenderer).toBeTruthy();
runner.expect(debugModule.DebugPanel).toBeTruthy();
// Set globals for other tests
global.ExtractedSectionManager = sectionModule.SectionManager;
global.ExtractedDOMRenderer = domModule.DOMRenderer;
global.ExtractedDebugPanel = debugModule.DebugPanel;
} catch (error) {
throw new Error(`Failed to load extracted components: ${error.message}`);
}
});
runner.it('should support debug panel with section editing workflow', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const DebugPanel = global.ExtractedDebugPanel;
// Setup DOM elements
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const debugContainer = document.createElement('div');
debugContainer.id = 'debug-messages-container';
debugContainer.style.display = 'none';
document.body.appendChild(debugContainer);
const debugButton = document.createElement('button');
debugButton.id = 'toggle-debug';
debugButton.textContent = '🔍 Debug';
document.body.appendChild(debugButton);
// Create components
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
// Test workflow: Create sections and debug them
const testMarkdown = '# Test Heading\nTest content for debugging';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
// Add debug messages
debugPanel.addMessage('Section created: ' + sections[0].id, 'INFO');
debugPanel.addMessage('DOM rendered successfully', 'SUCCESS');
runner.expect(debugPanel.getMessageCount()).toBe(2);
// Test showing debug panel
debugPanel.show();
runner.expect(debugPanel.isActive).toBeTruthy();
// Test debug panel content
const messages = debugPanel.getRecentMessages(2);
runner.expect(messages[0].message).toContain('Section created');
runner.expect(messages[1].message).toContain('DOM rendered');
// Cleanup
document.body.removeChild(container);
document.body.removeChild(debugContainer);
document.body.removeChild(debugButton);
});
runner.it('should support debug panel clearing and message management', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
// Add multiple messages
for (let i = 0; i < 10; i++) {
debugPanel.addMessage(`Test message ${i}`, i % 2 === 0 ? 'INFO' : 'WARNING');
}
runner.expect(debugPanel.getMessageCount()).toBe(10);
// Test getting recent messages
const recentFive = debugPanel.getRecentMessages(5);
runner.expect(recentFive.length).toBe(5);
runner.expect(recentFive[4].message).toContain('Test message 9');
// Test clearing
debugPanel.clear();
runner.expect(debugPanel.getMessageCount()).toBe(0);
});
runner.it('should handle debug panel DOM integration properly', () => {
const DebugPanel = global.ExtractedDebugPanel;
// Setup DOM
const debugContainer = document.createElement('div');
debugContainer.id = 'debug-messages-container';
debugContainer.style.display = 'none';
document.body.appendChild(debugContainer);
const debugButton = document.createElement('button');
debugButton.id = 'toggle-debug';
debugButton.textContent = '🔍 Debug';
debugButton.style.background = '#6c757d';
document.body.appendChild(debugButton);
const debugPanel = new DebugPanel();
// Test initial state
runner.expect(debugPanel.isActive).toBeFalsy();
runner.expect(debugContainer.style.display).toBe('none');
// Test toggle on
debugPanel.toggle();
runner.expect(debugPanel.isActive).toBeTruthy();
runner.expect(debugContainer.style.display).toBe('block');
runner.expect(debugButton.textContent).toContain('Debug (ON)');
// Test toggle off
debugPanel.toggle();
runner.expect(debugPanel.isActive).toBeFalsy();
runner.expect(debugContainer.style.display).toBe('none');
runner.expect(debugButton.textContent).toBe('🔍 Debug');
// Cleanup
document.body.removeChild(debugContainer);
document.body.removeChild(debugButton);
});
runner.it('should handle missing DOM elements gracefully', () => {
const DebugPanel = global.ExtractedDebugPanel;
const debugPanel = new DebugPanel();
// Try to toggle without DOM elements (should not throw)
try {
debugPanel.toggle();
debugPanel.show();
debugPanel.hide();
debugPanel.update();
runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
} catch (error) {
throw new Error(`DebugPanel should handle missing DOM gracefully: ${error.message}`);
}
});
runner.it('should support event-driven debug message addition', () => {
const SectionManager = global.ExtractedSectionManager;
const DebugPanel = global.ExtractedDebugPanel;
const sectionManager = new SectionManager();
const debugPanel = new DebugPanel();
// Listen to section manager events and add debug messages
let eventCount = 0;
sectionManager.on('sections-created', (data) => {
debugPanel.addMessage(`Sections created: ${data.count} sections`, 'INFO');
eventCount++;
});
sectionManager.on('edit-started', (data) => {
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
eventCount++;
});
// Create sections
const testMarkdown = '# Test\nContent';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
// Start editing
sectionManager.startEditing(sections[0].id);
// Verify debug messages were added
runner.expect(eventCount).toBe(2);
runner.expect(debugPanel.getMessageCount()).toBe(2);
const messages = debugPanel.getRecentMessages(2);
runner.expect(messages[0].message).toContain('Sections created');
runner.expect(messages[1].message).toContain('Edit started');
});
});
module.exports = runner;
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Running DebugPanel Integration Tests');
runner.run().then(() => {
console.log('✅ DebugPanel integration tests completed');
});
}

View File

@@ -1,193 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocumentNavigator TDD Test Runner</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
background-color: #f8f9fa;
}
.test-header {
background: white;
padding: 2rem;
border-radius: 8px;
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-output {
background: #1a1a1a;
color: #00ff00;
font-family: 'Courier New', monospace;
padding: 1rem;
border-radius: 8px;
margin-top: 1rem;
white-space: pre-wrap;
overflow-x: auto;
max-height: 400px;
}
.run-button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.run-button:hover {
background: #0056b3;
}
.run-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.status {
margin-top: 1rem;
padding: 1rem;
border-radius: 6px;
font-weight: bold;
}
.status.running {
background: #fff3cd;
color: #856404;
}
.status.passed {
background: #d4edda;
color: #155724;
}
.status.failed {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<div class="test-header">
<h1>📋 DocumentNavigator Widget TDD Test Suite</h1>
<p>
This test suite follows Test-Driven Development methodology to implement a Substack-style
floating document navigation widget. The tests define the expected behavior before
implementation begins.
</p>
<div>
<strong>Test Coverage:</strong>
<ul>
<li>✅ Widget class structure and inheritance</li>
<li>✅ Configuration and initialization</li>
<li>✅ DOM rendering and UI elements</li>
<li>✅ Heading extraction and hierarchy building</li>
<li>✅ Navigation functionality and smooth scrolling</li>
<li>✅ Expand/collapse behavior</li>
<li>✅ Scroll spy and active section detection</li>
<li>✅ Responsive behavior and auto-hide</li>
<li>✅ Keyboard navigation support</li>
<li>✅ Event emission and user interaction</li>
<li>✅ Edge cases and error handling</li>
</ul>
</div>
<button id="runTests" class="run-button">🧪 Run TDD Test Suite</button>
<div id="status" class="status" style="display: none;"></div>
</div>
<div id="testOutput" class="test-output" style="display: none;"></div>
<script type="module">
const runButton = document.getElementById('runTests');
const statusDiv = document.getElementById('status');
const outputDiv = document.getElementById('testOutput');
// Capture console output
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
let capturedOutput = '';
function captureConsole() {
capturedOutput = '';
console.log = (...args) => {
capturedOutput += args.join(' ') + '\n';
originalConsoleLog(...args);
};
console.error = (...args) => {
capturedOutput += 'ERROR: ' + args.join(' ') + '\n';
originalConsoleError(...args);
};
}
function restoreConsole() {
console.log = originalConsoleLog;
console.error = originalConsoleError;
}
function updateStatus(message, type) {
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.style.display = 'block';
}
function showOutput() {
outputDiv.textContent = capturedOutput;
outputDiv.style.display = 'block';
}
runButton.addEventListener('click', async () => {
runButton.disabled = true;
updateStatus('🧪 Running tests...', 'running');
captureConsole();
try {
// Import and run tests
const { runner } = await import('./test-document-navigator.js');
console.log('Starting DocumentNavigator TDD Test Suite...\n');
console.log('Note: Tests are expected to FAIL initially (Red phase of TDD)');
console.log('We will implement functionality to make them pass (Green phase).\n');
await runner.run();
if (runner.results.failed === 0) {
updateStatus(`🎉 All ${runner.results.total} tests passed!`, 'passed');
} else {
updateStatus(`${runner.results.failed} of ${runner.results.total} tests failed (Expected in TDD Red phase)`, 'failed');
}
} catch (error) {
console.error('Test execution failed:', error);
updateStatus('💥 Test execution failed - this is expected in TDD Red phase', 'failed');
} finally {
restoreConsole();
showOutput();
runButton.disabled = false;
}
});
// Auto-run tests on page load for development
document.addEventListener('DOMContentLoaded', () => {
console.log('DocumentNavigator TDD Test Runner loaded');
console.log('Ready to run tests - click the button above');
});
</script>
<!-- Test content for heading extraction tests -->
<div style="display: none;" id="test-content">
<h1>Test Chapter 1</h1>
<p>Sample content for testing heading extraction.</p>
<h2>Section 1.1</h2>
<h3>Subsection 1.1.1</h3>
<p>More sample content.</p>
<h2>Section 1.2</h2>
<h1>Test Chapter 2</h1>
</div>
</body>
</html>

View File

@@ -1,432 +0,0 @@
/**
* TDD Test Suite for DocumentNavigator Widget
*
* Tests the Substack-style floating navigation widget for document headings.
* Following TDD methodology: write tests first, then implement functionality.
*/
// Simple test runner for browser environment
class DocumentNavigatorTestRunner {
constructor() {
this.tests = [];
this.results = {
passed: 0,
failed: 0,
total: 0
};
}
test(name, testFn) {
this.tests.push({ name, testFn });
}
expect(actual) {
return {
toBe: (expected) => {
if (actual !== expected) {
throw new Error(`Expected ${actual} to be ${expected}`);
}
},
toBeInstanceOf: (expectedClass) => {
if (!(actual instanceof expectedClass)) {
throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`);
}
},
toBeTruthy: () => {
if (!actual) {
throw new Error(`Expected ${actual} to be truthy`);
}
},
toBeFalsy: () => {
if (actual) {
throw new Error(`Expected ${actual} to be falsy`);
}
},
toContain: (expected) => {
if (typeof actual === 'string' && !actual.includes(expected)) {
throw new Error(`Expected "${actual}" to contain "${expected}"`);
}
if (Array.isArray(actual) && !actual.includes(expected)) {
throw new Error(`Expected array to contain ${expected}`);
}
},
toHaveLength: (expected) => {
if (actual.length !== expected) {
throw new Error(`Expected length ${actual.length} to be ${expected}`);
}
},
toBeGreaterThan: (expected) => {
if (actual <= expected) {
throw new Error(`Expected ${actual} to be greater than ${expected}`);
}
}
};
}
async run() {
console.log('🧪 Running DocumentNavigator TDD Test Suite...\n');
for (const { name, testFn } of this.tests) {
this.results.total++;
try {
await testFn.call(this);
this.results.passed++;
console.log(`${name}`);
} catch (error) {
this.results.failed++;
console.log(`${name}`);
console.log(` ${error.message}\n`);
}
}
this.printSummary();
}
printSummary() {
console.log(`\n📊 Test Results:`);
console.log(` Passed: ${this.results.passed}`);
console.log(` Failed: ${this.results.failed}`);
console.log(` Total: ${this.results.total}`);
if (this.results.failed === 0) {
console.log(`\n🎉 All tests passed!`);
} else {
console.log(`\n${this.results.failed} test(s) failed.`);
}
}
}
// Create test runner
const runner = new DocumentNavigatorTestRunner();
// Test Suite: DocumentNavigator Widget
runner.test('DocumentNavigator class should exist and be importable', async function() {
// This test will fail initially - we haven't created the class yet
try {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
this.expect(DocumentNavigator).toBeTruthy();
this.expect(typeof DocumentNavigator).toBe('function');
} catch (error) {
throw new Error(`DocumentNavigator class not found: ${error.message}`);
}
});
runner.test('DocumentNavigator should extend UIWidget', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const { UIWidget } = await import('../widgets/base/UIWidget.js');
const navigator = new DocumentNavigator();
this.expect(navigator).toBeInstanceOf(UIWidget);
});
runner.test('DocumentNavigator should initialize with default configuration', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator();
// Test default configuration
this.expect(navigator.config.position).toBe('left');
this.expect(navigator.config.collapsed).toBe(true);
this.expect(navigator.config.autoHide).toBe(true);
this.expect(navigator.config.maxHeadingLevel).toBe(3);
this.expect(navigator.config.enableScrollSpy).toBe(true);
});
runner.test('DocumentNavigator should accept custom configuration', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const customConfig = {
position: 'right',
collapsed: false,
maxHeadingLevel: 4,
theme: 'dark'
};
const navigator = new DocumentNavigator(customConfig);
this.expect(navigator.config.position).toBe('right');
this.expect(navigator.config.collapsed).toBe(false);
this.expect(navigator.config.maxHeadingLevel).toBe(4);
this.expect(navigator.config.theme).toBe('dark');
});
runner.test('DocumentNavigator should render floating panel element', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator();
await navigator.render();
this.expect(navigator.element).toBeInstanceOf(HTMLElement);
this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy();
this.expect(navigator.element.style.position).toBe('fixed');
});
runner.test('DocumentNavigator should have toggle button in collapsed state', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator({ collapsed: true });
await navigator.render();
const toggleButton = navigator.findElement('.navigator-toggle');
this.expect(toggleButton).toBeInstanceOf(HTMLElement);
this.expect(toggleButton.style.display).not.toBe('none');
const navList = navigator.findElement('.navigator-list');
this.expect(navList.style.display).toBe('none');
});
runner.test('DocumentNavigator should extract headings from document', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
// Create test document with headings
const testContainer = document.createElement('div');
testContainer.innerHTML = `
<h1 id="heading1">First Heading</h1>
<p>Some content</p>
<h2 id="heading2">Second Heading</h2>
<h3 id="heading3">Third Heading</h3>
<p>More content</p>
<h2 id="heading4">Fourth Heading</h2>
`;
document.body.appendChild(testContainer);
const navigator = new DocumentNavigator({
container: testContainer,
maxHeadingLevel: 3
});
const headings = navigator.extractHeadings();
this.expect(headings).toHaveLength(4);
this.expect(headings[0].tagName).toBe('H1');
this.expect(headings[0].textContent).toBe('First Heading');
this.expect(headings[1].tagName).toBe('H2');
this.expect(headings[2].tagName).toBe('H3');
this.expect(headings[3].tagName).toBe('H2');
// Cleanup
document.body.removeChild(testContainer);
});
runner.test('DocumentNavigator should build navigation hierarchy', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
// Create test document with nested headings
const testContainer = document.createElement('div');
testContainer.innerHTML = `
<h1>Chapter 1</h1>
<h2>Section 1.1</h2>
<h3>Subsection 1.1.1</h3>
<h3>Subsection 1.1.2</h3>
<h2>Section 1.2</h2>
<h1>Chapter 2</h1>
`;
document.body.appendChild(testContainer);
const navigator = new DocumentNavigator({ container: testContainer });
await navigator.render();
const navItems = navigator.buildNavigationTree();
// Should have hierarchical structure
this.expect(navItems).toHaveLength(2); // 2 H1 elements
this.expect(navItems[0].children).toHaveLength(2); // 2 H2 under first H1
this.expect(navItems[0].children[0].children).toHaveLength(2); // 2 H3 under first H2
// Cleanup
document.body.removeChild(testContainer);
});
runner.test('DocumentNavigator should handle click navigation', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
// Create test document
const testContainer = document.createElement('div');
testContainer.innerHTML = `
<h1 id="target-heading">Target Heading</h1>
<p style="height: 1000px;">Spacer content</p>
`;
document.body.appendChild(testContainer);
const navigator = new DocumentNavigator({ container: testContainer });
await navigator.render();
// Simulate click on navigation item
const navItem = navigator.findElement('[data-target="target-heading"]');
this.expect(navItem).toBeTruthy();
// Mock scrollIntoView for testing
const targetElement = document.getElementById('target-heading');
let scrollCalled = false;
targetElement.scrollIntoView = () => { scrollCalled = true; };
// Click navigation item
navItem.click();
this.expect(scrollCalled).toBeTruthy();
// Cleanup
document.body.removeChild(testContainer);
});
runner.test('DocumentNavigator should support expand/collapse functionality', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator({ collapsed: true });
await navigator.render();
// Should start collapsed
this.expect(navigator.isCollapsed).toBeTruthy();
const toggleButton = navigator.findElement('.navigator-toggle');
const navList = navigator.findElement('.navigator-list');
// Toggle to expanded
await navigator.expand();
this.expect(navigator.isCollapsed).toBeFalsy();
this.expect(navList.style.display).not.toBe('none');
// Toggle back to collapsed
await navigator.collapse();
this.expect(navigator.isCollapsed).toBeTruthy();
this.expect(navList.style.display).toBe('none');
});
runner.test('DocumentNavigator should implement scroll spy functionality', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
// Create test document with multiple sections
const testContainer = document.createElement('div');
testContainer.innerHTML = `
<div style="height: 100px;"></div>
<h1 id="section1">Section 1</h1>
<div style="height: 400px;"></div>
<h2 id="section2">Section 2</h2>
<div style="height: 400px;"></div>
<h2 id="section3">Section 3</h2>
<div style="height: 400px;"></div>
`;
document.body.appendChild(testContainer);
const navigator = new DocumentNavigator({
container: testContainer,
enableScrollSpy: true
});
await navigator.render();
// Test current section detection
const currentSection = navigator.getCurrentSection();
this.expect(currentSection).toBeTruthy();
// Cleanup
document.body.removeChild(testContainer);
});
runner.test('DocumentNavigator should handle responsive behavior', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator({ autoHide: true });
await navigator.render();
// Mock viewport resize
const originalInnerWidth = window.innerWidth;
// Test mobile viewport
Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true });
navigator.handleResize();
this.expect(navigator.element.style.display).toBe('none');
// Test desktop viewport
Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true });
navigator.handleResize();
this.expect(navigator.element.style.display).not.toBe('none');
// Restore original
Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true });
});
runner.test('DocumentNavigator should provide keyboard navigation support', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator();
await navigator.render();
// Test keyboard shortcuts
let expandCalled = false;
let collapseCalled = false;
navigator.expand = async () => { expandCalled = true; };
navigator.collapse = async () => { collapseCalled = true; };
// Simulate keyboard events
const element = navigator.element;
// Test Escape key (should collapse)
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
element.dispatchEvent(escapeEvent);
this.expect(collapseCalled).toBeTruthy();
// Test Enter/Space key (should expand)
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
element.dispatchEvent(enterEvent);
this.expect(expandCalled).toBeTruthy();
});
runner.test('DocumentNavigator should emit events for user interactions', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
const navigator = new DocumentNavigator();
await navigator.render();
// Test event emission
let navigationEvent = null;
navigator.addEventListener('navigate', (e) => {
navigationEvent = e;
});
let toggleEvent = null;
navigator.addEventListener('toggle', (e) => {
toggleEvent = e;
});
// Trigger navigation
navigator.navigateToHeading('test-heading');
this.expect(navigationEvent).toBeTruthy();
this.expect(navigationEvent.detail.target).toBe('test-heading');
// Trigger toggle
await navigator.toggle();
this.expect(toggleEvent).toBeTruthy();
});
runner.test('DocumentNavigator should handle empty document gracefully', async function() {
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
// Create empty container
const emptyContainer = document.createElement('div');
document.body.appendChild(emptyContainer);
const navigator = new DocumentNavigator({ container: emptyContainer });
const headings = navigator.extractHeadings();
this.expect(headings).toHaveLength(0);
await navigator.render();
const navList = navigator.findElement('.navigator-list');
this.expect(navList.children).toHaveLength(0);
// Should show empty state message
const emptyMessage = navigator.findElement('.navigator-empty');
this.expect(emptyMessage).toBeTruthy();
// Cleanup
document.body.removeChild(emptyContainer);
});
// Export test runner for use in HTML
window.runDocumentNavigatorTests = () => runner.run();
console.log('📋 DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()');
export { runner };

View File

@@ -1,218 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test for Document Controls Component Extraction
*
* Tests the extraction of DocumentControls from the monolithic editor.js
* DocumentControls handles the floating control panel and its actions.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
// Define expected DocumentControls API
const EXPECTED_DOCUMENTCONTROLS_API = [
'constructor',
'create',
'destroy',
'show',
'hide',
'addButton',
'removeButton',
'setEventHandlers',
'updateStatus',
'getControlPanel'
];
runner.describe('DocumentControls Component Extraction', () => {
runner.it('should define expected API methods', () => {
const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API;
runner.expect(expectedMethods.length).toBe(10);
runner.expect(expectedMethods).toContain('create');
runner.expect(expectedMethods).toContain('addButton');
runner.expect(expectedMethods).toContain('setEventHandlers');
});
runner.it('should load extracted DocumentControls component', () => {
// Load the extracted component
delete require.cache[require.resolve('../components/document-controls.js')];
try {
const module = require('../components/document-controls.js');
runner.expect(module.DocumentControls).toBeTruthy();
// Set global for other tests
global.ExtractedDocumentControls = module.DocumentControls;
} catch (error) {
throw new Error(`Failed to load extracted DocumentControls: ${error.message}`);
}
});
runner.it('should preserve constructor functionality', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
runner.expect(controls).toBeInstanceOf(DocumentControls);
runner.expect(controls.controlPanel).toBeFalsy(); // Initially null
runner.expect(controls.buttons).toBeInstanceOf(Map);
});
runner.it('should preserve control panel creation functionality', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
const panel = controls.getControlPanel();
runner.expect(panel).toBeTruthy();
runner.expect(panel.id).toBe('markitect-global-controls');
// Check that panel is added to DOM
const domPanel = document.getElementById('markitect-global-controls');
runner.expect(domPanel).toBeTruthy();
// Cleanup
controls.destroy();
});
runner.it('should preserve button creation functionality', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
// Default buttons should be created
runner.expect(controls.buttons.has('save-document')).toBeTruthy();
runner.expect(controls.buttons.has('reset-all')).toBeTruthy();
runner.expect(controls.buttons.has('show-status')).toBeTruthy();
runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy();
// Check DOM elements exist
runner.expect(document.getElementById('save-document')).toBeTruthy();
runner.expect(document.getElementById('reset-all')).toBeTruthy();
runner.expect(document.getElementById('show-status')).toBeTruthy();
runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
// Cleanup
controls.destroy();
});
runner.it('should support custom button addition', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
// Add custom button
const customButton = controls.addButton('custom-test', '🎯 Test', '#ff6600');
runner.expect(customButton).toBeTruthy();
runner.expect(customButton.id).toBe('custom-test');
runner.expect(customButton.textContent).toBe('🎯 Test');
// Check button is in map and DOM
runner.expect(controls.buttons.has('custom-test')).toBeTruthy();
runner.expect(document.getElementById('custom-test')).toBeTruthy();
// Cleanup
controls.destroy();
});
runner.it('should support event handler configuration', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
let saveClicked = false;
let resetClicked = false;
const handlers = {
'save-document': () => { saveClicked = true; },
'reset-all': () => { resetClicked = true; }
};
controls.setEventHandlers(handlers);
// Simulate button clicks
const saveBtn = document.getElementById('save-document');
const resetBtn = document.getElementById('reset-all');
saveBtn.click();
resetBtn.click();
runner.expect(saveClicked).toBeTruthy();
runner.expect(resetClicked).toBeTruthy();
// Cleanup
controls.destroy();
});
runner.it('should support show/hide functionality', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
const panel = controls.getControlPanel();
// Test hiding
controls.hide();
runner.expect(panel.style.display).toBe('none');
// Test showing
controls.show();
runner.expect(panel.style.display).toBe('block');
// Cleanup
controls.destroy();
});
runner.it('should preserve destroy functionality', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
// Verify panel exists
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
// Destroy
controls.destroy();
// Verify panel is removed
runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
runner.expect(controls.controlPanel).toBeFalsy();
});
runner.it('should support status updates', () => {
const DocumentControls = global.ExtractedDocumentControls;
const controls = new DocumentControls();
controls.create();
// Test status update
controls.updateStatus({ totalSections: 5, editingSections: 2 });
// The status should be reflected in the panel (implementation specific)
const panel = controls.getControlPanel();
runner.expect(panel).toBeTruthy();
// Cleanup
controls.destroy();
});
});
module.exports = {
runner,
EXPECTED_DOCUMENTCONTROLS_API
};
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Testing DocumentControls Component Extraction');
runner.run().then(() => {
console.log('✅ DocumentControls extraction tests completed');
});
}

View File

@@ -1,212 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test for DOMRenderer Component Extraction
*
* Tests the extraction of DOMRenderer from the monolithic editor.js
* DOMRenderer handles all DOM interactions and UI rendering.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
// Define expected DOMRenderer API
const EXPECTED_DOMRENDERER_API = [
'constructor',
'renderAllSections',
'renderSection',
'showEditor',
'hideCurrentEditor',
'showImageEditor',
'findSectionElement',
'handleSectionClick',
'setupSectionElement',
'trackEvent',
'getEventStats'
// Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer
];
runner.describe('DOMRenderer Component Extraction', () => {
runner.it('should define expected API methods', () => {
const expectedMethods = EXPECTED_DOMRENDERER_API;
runner.expect(expectedMethods.length).toBe(11);
runner.expect(expectedMethods).toContain('renderAllSections');
runner.expect(expectedMethods).toContain('showEditor');
runner.expect(expectedMethods).toContain('handleSectionClick');
});
runner.it('should extract from monolithic editor.js', () => {
// Load the monolithic editor.js to extract DOMRenderer
delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
try {
const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
runner.expect(editorModule.DOMRenderer).toBeTruthy();
// Set global for other tests
global.DOMRenderer = editorModule.DOMRenderer;
global.SectionManager = editorModule.SectionManager;
} catch (error) {
throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
}
});
runner.it('should preserve DOMRenderer constructor functionality', () => {
const DOMRenderer = global.DOMRenderer;
const SectionManager = global.SectionManager;
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
runner.expect(renderer).toBeInstanceOf(DOMRenderer);
runner.expect(renderer.sectionManager).toBe(sectionManager);
runner.expect(renderer.container).toBe(container);
});
runner.it('should preserve section rendering functionality', () => {
const DOMRenderer = global.DOMRenderer;
const SectionManager = global.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
// This should not throw an error
renderer.renderAllSections(sections);
// Check that some content was rendered
runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check
});
runner.it('should preserve findSectionElement functionality', () => {
const DOMRenderer = global.DOMRenderer;
const SectionManager = global.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
renderer.renderAllSections(sections);
const sectionId = sections[0].id;
const element = renderer.findSectionElement(sectionId);
// Should find an element or return null (not throw error)
runner.expect(typeof element === 'object').toBeTruthy();
});
runner.it('should preserve event tracking functionality', () => {
const DOMRenderer = global.DOMRenderer;
const SectionManager = global.SectionManager;
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
// Should have trackEvent method
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
// Should be able to track an event
renderer.trackEvent('test-event', { data: 'test' });
// Should have getEventStats method
runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
const stats = renderer.getEventStats();
runner.expect(typeof stats === 'object').toBeTruthy();
});
runner.it('should preserve editor showing functionality', () => {
const DOMRenderer = global.DOMRenderer;
const SectionManager = global.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
renderer.renderAllSections(sections);
const sectionId = sections[0].id;
// showEditor should not throw error
try {
renderer.showEditor(sectionId, 'test content');
runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
} catch (error) {
// Some errors are expected if DOM structure isn't complete
runner.expect(typeof error.message === 'string').toBeTruthy();
}
});
runner.it('should have core DOM rendering methods', () => {
const DOMRenderer = global.DOMRenderer;
const SectionManager = global.SectionManager;
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
// Should have core methods
runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy();
runner.expect(typeof renderer.showEditor === 'function').toBeTruthy();
runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy();
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
});
});
// Export API tests for use during extraction
const DOMRENDERER_API_TESTS = [
(DOMRenderer, SectionManager) => {
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
if (!renderer.sectionManager) {
throw new Error('sectionManager property missing');
}
},
(DOMRenderer, SectionManager) => {
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
if (typeof renderer.renderAllSections !== 'function') {
throw new Error('renderAllSections method missing');
}
},
(DOMRenderer, SectionManager) => {
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
if (typeof renderer.showEditor !== 'function') {
throw new Error('showEditor method missing');
}
}
];
module.exports = {
runner,
EXPECTED_DOMRENDERER_API,
DOMRENDERER_API_TESTS
};
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Testing DOMRenderer Component Extraction');
runner.run().then(() => {
console.log('✅ DOMRenderer extraction tests completed');
});
}

View File

@@ -1,271 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test for Extracted DOMRenderer Component
*
* Tests the extracted DOMRenderer component independently from the monolith.
* Verifies that core functionality is preserved after extraction.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
runner.describe('Extracted DOMRenderer Component', () => {
runner.it('should load extracted DOMRenderer component', () => {
// Load the extracted component
delete require.cache[require.resolve('../components/dom-renderer.js')];
try {
const module = require('../components/dom-renderer.js');
runner.expect(module.DOMRenderer).toBeTruthy();
runner.expect(module.FloatingMenu).toBeTruthy();
// Set globals for other tests
global.ExtractedDOMRenderer = module.DOMRenderer;
global.ExtractedFloatingMenu = module.FloatingMenu;
} catch (error) {
throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`);
}
});
runner.it('should preserve constructor functionality', () => {
const DOMRenderer = global.ExtractedDOMRenderer;
// Load SectionManager from our extracted core
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
runner.expect(renderer).toBeInstanceOf(DOMRenderer);
runner.expect(renderer.sectionManager).toBe(sectionManager);
runner.expect(renderer.container).toBe(container);
runner.expect(renderer.editingSections).toBeInstanceOf(Set);
});
runner.it('should preserve section rendering functionality', () => {
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
// This should not throw an error
renderer.renderAllSections(sections);
// Check that content was rendered
runner.expect(container.innerHTML.length > 100).toBeTruthy();
runner.expect(container.innerHTML).toContain('Test Heading');
});
runner.it('should preserve findSectionElement functionality', () => {
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
renderer.renderAllSections(sections);
const sectionId = sections[0].id;
const element = renderer.findSectionElement(sectionId);
runner.expect(element).toBeTruthy();
runner.expect(element.getAttribute('data-section-id')).toBe(sectionId);
});
runner.it('should preserve event tracking functionality', () => {
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
// Should have trackEvent method
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
// Should be able to track an event
renderer.trackEvent('test-event', { data: 'test' });
// Should have getEventStats method
runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
const stats = renderer.getEventStats();
runner.expect(typeof stats === 'object').toBeTruthy();
runner.expect(stats).toHaveProperty('stats');
runner.expect(stats).toHaveProperty('totalEvents');
runner.expect(stats).toHaveProperty('recentEvents');
});
runner.it('should preserve editor showing functionality', () => {
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
renderer.renderAllSections(sections);
const sectionId = sections[0].id;
// showEditor should not throw error
try {
renderer.showEditor(sectionId, 'test content');
runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
// Check that editing state was set
runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy();
} catch (error) {
throw new Error(`showEditor failed: ${error.message}`);
}
});
runner.it('should preserve FloatingMenu functionality', () => {
const FloatingMenu = global.ExtractedFloatingMenu;
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
renderer.renderAllSections(sections);
const sectionId = sections[0].id;
const floatingMenu = new FloatingMenu(sectionId, 'text', renderer);
runner.expect(floatingMenu.sectionId).toBe(sectionId);
runner.expect(floatingMenu.type).toBe('text');
runner.expect(floatingMenu.renderer).toBe(renderer);
runner.expect(floatingMenu.isVisible).toBeFalsy();
// Test show/hide functionality
const content = document.createElement('div');
content.textContent = 'Test content';
floatingMenu.show(content);
runner.expect(floatingMenu.isVisible).toBeTruthy();
floatingMenu.hide();
runner.expect(floatingMenu.isVisible).toBeFalsy();
});
runner.it('should handle section click events', () => {
const DOMRenderer = global.ExtractedDOMRenderer;
const sectionModule = require('../core/section-manager.js');
const SectionManager = sectionModule.SectionManager;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
const sectionManager = new SectionManager();
const renderer = new DOMRenderer(sectionManager, container);
const testMarkdown = '# Test Heading\nTest content';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
renderer.renderAllSections(sections);
const sectionId = sections[0].id;
const element = renderer.findSectionElement(sectionId);
// Simulate a click event
const clickEvent = new Event('click', { bubbles: true });
Object.defineProperty(clickEvent, 'target', { value: element });
// Should not throw error
try {
renderer.handleSectionClick(clickEvent);
runner.expect(true).toBeTruthy();
} catch (error) {
throw new Error(`handleSectionClick failed: ${error.message}`);
}
});
// Comparative test - verify extracted component behaves similarly to original
runner.it('should behave similarly to original monolithic component', () => {
// Load both components
const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
const extractedModule = require('../components/dom-renderer.js');
const sectionModule = require('../core/section-manager.js');
const originalSectionManager = new originalModule.SectionManager();
const extractedSectionManager = new sectionModule.SectionManager();
const originalContainer = document.createElement('div');
originalContainer.innerHTML = '<div id="markdown-content"></div>';
const extractedContainer = document.createElement('div');
extractedContainer.innerHTML = '<div id="markdown-content"></div>';
const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer);
const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer);
const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
// Create sections with both
const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown);
const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown);
// Render with both
originalRenderer.renderAllSections(originalSections);
extractedRenderer.renderAllSections(extractedSections);
// Should have rendered content
runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy();
runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy();
// Should have same number of section elements
const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section');
const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section');
runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length);
// Should have similar event stats structure
const originalStats = originalRenderer.getEventStats();
const extractedStats = extractedRenderer.getEventStats();
runner.expect(extractedStats).toHaveProperty('stats');
runner.expect(extractedStats).toHaveProperty('totalEvents');
runner.expect(extractedStats).toHaveProperty('recentEvents');
});
});
module.exports = runner;
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Testing Extracted DOMRenderer Component');
runner.run().then(() => {
console.log('✅ Extracted DOMRenderer tests completed');
});
}

View File

@@ -1,226 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test for Extracted SectionManager Component
*
* Tests the extracted SectionManager component independently from the monolith.
* Verifies that all functionality is preserved after extraction.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
runner.describe('Extracted SectionManager Component', () => {
runner.it('should load extracted SectionManager component', () => {
// Load the extracted component
delete require.cache[require.resolve('../core/section-manager.js')];
try {
const module = require('../core/section-manager.js');
runner.expect(module.SectionManager).toBeTruthy();
runner.expect(module.Section).toBeTruthy();
runner.expect(module.EditState).toBeTruthy();
runner.expect(module.SectionType).toBeTruthy();
// Set globals for other tests
global.ExtractedSectionManager = module.SectionManager;
global.ExtractedSection = module.Section;
global.ExtractedEditState = module.EditState;
global.ExtractedSectionType = module.SectionType;
} catch (error) {
throw new Error(`Failed to load extracted SectionManager: ${error.message}`);
}
});
runner.it('should preserve constructor functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
runner.expect(manager).toBeInstanceOf(SectionManager);
runner.expect(manager.sections).toBeInstanceOf(Map);
runner.expect(manager.listeners).toBeInstanceOf(Map);
});
runner.it('should preserve section creation functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
const sections = manager.createSectionsFromMarkdown(testMarkdown);
runner.expect(Array.isArray(sections)).toBeTruthy();
runner.expect(sections.length).toBe(2);
runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
});
runner.it('should preserve section editing functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
const sections = manager.createSectionsFromMarkdown('# Test\nContent');
const sectionId = sections[0].id;
// Test start editing
const content = manager.startEditing(sectionId);
runner.expect(content).toContain('Test');
const section = manager.sections.get(sectionId);
runner.expect(section.isEditing()).toBeTruthy();
// Test stop editing
section.stopEditing();
runner.expect(section.isEditing()).toBeFalsy();
});
runner.it('should preserve event system functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
let eventFired = false;
let eventData = null;
manager.on('test-event', (data) => {
eventFired = true;
eventData = data;
});
manager.emit('test-event', { test: 'data' });
runner.expect(eventFired).toBeTruthy();
runner.expect(eventData).toEqual({ test: 'data' });
});
runner.it('should preserve document status functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
manager.createSectionsFromMarkdown('# Test\nContent');
const status = manager.getDocumentStatus();
runner.expect(status).toHaveProperty('totalSections');
runner.expect(status).toHaveProperty('editingSections');
runner.expect(status.totalSections).toBe(1);
});
runner.it('should preserve getAllSections functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
const testMarkdown = '# One\nContent\n\n# Two\nMore content';
manager.createSectionsFromMarkdown(testMarkdown);
const allSections = manager.getAllSections();
runner.expect(Array.isArray(allSections)).toBeTruthy();
runner.expect(allSections.length).toBe(2);
});
runner.it('should preserve section splitting functionality', () => {
const SectionManager = global.ExtractedSectionManager;
const manager = new SectionManager();
const sections = manager.createSectionsFromMarkdown('# Original\nContent');
const sectionId = sections[0].id;
const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
const newSections = manager.handleSectionSplit(sectionId, newContent);
runner.expect(Array.isArray(newSections)).toBeTruthy();
runner.expect(newSections.length).toBe(2);
runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
});
runner.it('should preserve Section class functionality', () => {
const Section = global.ExtractedSection;
const EditState = global.ExtractedEditState;
const section = new Section('test-id', '# Test Content', 'heading');
runner.expect(section.id).toBe('test-id');
runner.expect(section.currentMarkdown).toBe('# Test Content');
runner.expect(section.type).toBe('heading');
runner.expect(section.state).toBe(EditState.ORIGINAL);
});
runner.it('should preserve Section ID generation', () => {
const Section = global.ExtractedSection;
const id1 = Section.generateId('# Test Heading', 0);
const id2 = Section.generateId('# Different Heading', 1);
runner.expect(typeof id1 === 'string').toBeTruthy();
runner.expect(typeof id2 === 'string').toBeTruthy();
runner.expect(id1).toContain('section-');
runner.expect(id2).toContain('section-');
runner.expect(id1 !== id2).toBeTruthy(); // Should be unique
});
runner.it('should preserve Section type detection', () => {
const Section = global.ExtractedSection;
const SectionType = global.ExtractedSectionType;
runner.expect(Section.detectType('# Heading')).toBe(SectionType.HEADING);
runner.expect(Section.detectType('![Image](url)')).toBe(SectionType.IMAGE);
runner.expect(Section.detectType('```code```')).toBe(SectionType.CODE);
runner.expect(Section.detectType('Regular paragraph')).toBe(SectionType.PARAGRAPH);
});
// Comparative test - verify extracted component behaves identically to original
runner.it('should behave identically to original monolithic component', () => {
// Load both components
const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
const extractedModule = require('../core/section-manager.js');
const originalManager = new originalModule.SectionManager();
const extractedManager = new extractedModule.SectionManager();
const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
// Debug: Check what each component produces
console.log('Creating sections with original component...');
const originalSections = originalManager.createSectionsFromMarkdown(testMarkdown);
console.log(`Original produced ${originalSections.length} sections`);
console.log('Creating sections with extracted component...');
const extractedSections = extractedManager.createSectionsFromMarkdown(testMarkdown);
console.log(`Extracted produced ${extractedSections.length} sections`);
if (originalSections.length > 0) {
console.log('Original first section:', originalSections[0].currentMarkdown);
}
if (extractedSections.length > 0) {
console.log('Extracted first section:', extractedSections[0].currentMarkdown);
}
// Should have same number of sections
runner.expect(extractedSections.length).toBe(originalSections.length);
// Should have same content
for (let i = 0; i < originalSections.length; i++) {
runner.expect(extractedSections[i].currentMarkdown).toBe(originalSections[i].currentMarkdown);
runner.expect(extractedSections[i].type).toBe(originalSections[i].type);
}
// Should have same document status structure
const originalStatus = originalManager.getDocumentStatus();
const extractedStatus = extractedManager.getDocumentStatus();
console.log('Original status:', originalStatus);
console.log('Extracted status:', extractedStatus);
runner.expect(extractedStatus.totalSections).toBe(originalStatus.totalSections);
runner.expect(extractedStatus.editingSections).toBe(originalStatus.editingSections);
});
});
module.exports = runner;
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Testing Extracted SectionManager Component');
runner.run().then(() => {
console.log('✅ Extracted SectionManager tests completed');
});
}

View File

@@ -1,305 +0,0 @@
#!/usr/bin/env node
/**
* Full Integration Test
*
* Tests that all extracted components (SectionManager, DOMRenderer,
* DebugPanel, DocumentControls) work together as a complete system.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
runner.describe('Full Component Integration Tests', () => {
runner.it('should load all extracted components', () => {
try {
// Load all extracted components
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
const debugModule = require('../components/debug-panel.js');
const controlsModule = require('../components/document-controls.js');
runner.expect(sectionModule.SectionManager).toBeTruthy();
runner.expect(domModule.DOMRenderer).toBeTruthy();
runner.expect(debugModule.DebugPanel).toBeTruthy();
runner.expect(controlsModule.DocumentControls).toBeTruthy();
// Set globals for other tests
global.ExtractedSectionManager = sectionModule.SectionManager;
global.ExtractedDOMRenderer = domModule.DOMRenderer;
global.ExtractedDebugPanel = debugModule.DebugPanel;
global.ExtractedDocumentControls = controlsModule.DocumentControls;
} catch (error) {
throw new Error(`Failed to load extracted components: ${error.message}`);
}
});
runner.it('should support complete document editing workflow with all components', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const DebugPanel = global.ExtractedDebugPanel;
const DocumentControls = global.ExtractedDocumentControls;
// Setup DOM container
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
// Create all components
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
// Setup document controls
documentControls.create();
// Wire up event handlers for debugging
sectionManager.on('sections-created', (data) => {
debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
});
sectionManager.on('edit-started', (data) => {
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
});
// Test workflow: Create document
const testMarkdown = `# Document Title
Introduction paragraph with some content.
## Section A
Content for section A with details.
![Test Image](https://example.com/test.jpg)
### Subsection A.1
More detailed content here.`;
// Create sections
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
runner.expect(sections.length).toBe(4);
// Render sections
domRenderer.renderAllSections(sections);
const renderedElements = container.querySelectorAll('.ui-edit-section');
runner.expect(renderedElements.length).toBe(sections.length);
// Test editing workflow
const firstSection = sections[0];
sectionManager.startEditing(firstSection.id);
runner.expect(firstSection.isEditing()).toBeTruthy();
// Check debug messages were created
runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started
// Test document controls functionality
const controlPanel = documentControls.getControlPanel();
runner.expect(controlPanel).toBeTruthy();
runner.expect(document.getElementById('save-document')).toBeTruthy();
runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
// Cleanup
document.body.removeChild(container);
documentControls.destroy();
});
runner.it('should support debug panel integration with document controls', () => {
const DebugPanel = global.ExtractedDebugPanel;
const DocumentControls = global.ExtractedDocumentControls;
// Create components
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
// Setup document controls
documentControls.create();
// Setup debug panel toggle handler
const handlers = {
'toggle-debug': () => debugPanel.toggle()
};
documentControls.setEventHandlers(handlers);
// Test debug toggle functionality
const debugButton = documentControls.getButton('toggle-debug');
runner.expect(debugButton).toBeTruthy();
// Add some debug messages
debugPanel.addMessage('Test message 1', 'INFO');
debugPanel.addMessage('Test message 2', 'ERROR');
// Simulate button click to show debug panel
debugButton.click();
runner.expect(debugPanel.isActive).toBeTruthy();
// Simulate button click to hide debug panel
debugButton.click();
runner.expect(debugPanel.isActive).toBeFalsy();
// Cleanup
documentControls.destroy();
});
runner.it('should support event-driven communication between all components', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const DebugPanel = global.ExtractedDebugPanel;
const DocumentControls = global.ExtractedDocumentControls;
// Setup container
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
// Create components
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
documentControls.create();
// Setup comprehensive event handling
let eventLog = [];
sectionManager.on('sections-created', (data) => {
eventLog.push(`sections-created: ${data.count} sections`);
debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO');
});
sectionManager.on('edit-started', (data) => {
eventLog.push(`edit-started: ${data.sectionId}`);
debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG');
});
sectionManager.on('changes-accepted', (data) => {
eventLog.push(`changes-accepted: ${data.sectionId}`);
debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS');
});
// Test complete workflow
const testMarkdown = '# Test\nContent for testing';
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
// Start editing
sectionManager.startEditing(sections[0].id);
sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content');
sectionManager.acceptChanges(sections[0].id);
// Verify events were logged
runner.expect(eventLog.length).toBe(3);
runner.expect(eventLog[0]).toContain('sections-created');
runner.expect(eventLog[1]).toContain('edit-started');
runner.expect(eventLog[2]).toContain('changes-accepted');
// Verify debug messages were created
runner.expect(debugPanel.getMessageCount()).toBe(3);
// Test document controls status update
const status = sectionManager.getDocumentStatus();
documentControls.updateStatus(status);
runner.expect(documentControls.lastStatus).toBeTruthy();
// Cleanup
document.body.removeChild(container);
documentControls.destroy();
});
runner.it('should handle error scenarios gracefully across components', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const DebugPanel = global.ExtractedDebugPanel;
const DocumentControls = global.ExtractedDocumentControls;
// Test component creation without proper DOM setup
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
// These should not throw errors
try {
debugPanel.toggle(); // No DOM elements
debugPanel.update(); // No DOM elements
documentControls.show(); // No control panel created yet
documentControls.hide(); // No control panel created yet
runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
} catch (error) {
throw new Error(`Components should handle missing DOM gracefully: ${error.message}`);
}
// Test section manager with invalid input
const sectionManager = new SectionManager();
const sections = sectionManager.createSectionsFromMarkdown('');
runner.expect(sections.length).toBe(0);
// Test DOM renderer with invalid container
try {
const invalidRenderer = new DOMRenderer(sectionManager, null);
runner.expect(invalidRenderer.container).toBeFalsy();
} catch (error) {
// This is acceptable - constructor might validate input
runner.expect(typeof error.message === 'string').toBeTruthy();
}
});
runner.it('should support scalable architecture with component lifecycle', () => {
const SectionManager = global.ExtractedSectionManager;
const DOMRenderer = global.ExtractedDOMRenderer;
const DebugPanel = global.ExtractedDebugPanel;
const DocumentControls = global.ExtractedDocumentControls;
// Test multiple instances
const sectionManager1 = new SectionManager();
const sectionManager2 = new SectionManager();
const debugPanel1 = new DebugPanel();
const debugPanel2 = new DebugPanel();
// Each should be independent
debugPanel1.addMessage('Message from panel 1', 'INFO');
debugPanel2.addMessage('Message from panel 2', 'ERROR');
runner.expect(debugPanel1.getMessageCount()).toBe(1);
runner.expect(debugPanel2.getMessageCount()).toBe(1);
// Test section managers are independent
const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1');
const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2');
runner.expect(sections1.length).toBe(1);
runner.expect(sections2.length).toBe(1);
runner.expect(sections1[0]).toBeTruthy();
runner.expect(sections2[0]).toBeTruthy();
// IDs should be different (each section gets unique ID)
const id1 = sections1[0].id;
const id2 = sections2[0].id;
runner.expect(id1 !== id2).toBeTruthy();
// Test document controls lifecycle
const controls1 = new DocumentControls();
const controls2 = new DocumentControls();
controls1.create();
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
controls2.create(); // Should replace the first one
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
controls2.destroy();
runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
});
});
module.exports = runner;
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Running Full Component Integration Tests');
runner.run().then(() => {
console.log('✅ Full integration tests completed');
});
}

View File

@@ -1,342 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocumentNavigator Live Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
color: #333;
}
.demo-header {
text-align: center;
background: #f8f9fa;
padding: 2rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.demo-content {
margin-top: 3rem;
}
h1, h2, h3 {
scroll-margin-top: 100px; /* Account for navigator */
}
h1 {
color: #2c3e50;
border-bottom: 3px solid #3498db;
padding-bottom: 0.5rem;
}
h2 {
color: #34495e;
margin-top: 3rem;
}
h3 {
color: #7f8c8d;
margin-top: 2rem;
}
.content-section {
margin-bottom: 3rem;
}
.highlight {
background: #fff3cd;
padding: 1rem;
border-radius: 4px;
border-left: 4px solid #ffc107;
margin: 1rem 0;
}
code {
background: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Monaco', 'Consolas', monospace;
}
</style>
</head>
<body>
<div class="demo-header">
<h1>📋 DocumentNavigator Live Demo</h1>
<p>This page demonstrates the Substack-style floating navigation widget in action.</p>
<p><strong>Look for the hamburger menu (☰) on the left side!</strong></p>
<div class="highlight">
<strong>Features to test:</strong><br>
• Click the hamburger menu to expand navigation<br>
• Click any heading in the navigator to jump to it<br>
• Scroll and watch the current section highlight<br>
• Try keyboard shortcuts (Enter/Space to toggle, Escape to close)<br>
• Resize window to test responsive behavior
</div>
</div>
<div id="markdown-content" class="demo-content">
<h1 id="introduction">1. Introduction to MarkiTect</h1>
<div class="content-section">
<p>MarkiTect is an advanced markdown processing engine that provides sophisticated document management capabilities. This demo showcases the DocumentNavigator widget, which provides Substack-style navigation for long-form documents.</p>
<p>The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.</p>
</div>
<h2 id="features">1.1 Core Features</h2>
<div class="content-section">
<p>The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:</p>
<ul>
<li><strong>Automatic Heading Detection</strong>: Scans document for H1, H2, H3 elements</li>
<li><strong>Hierarchical Structure</strong>: Maintains proper heading hierarchy with indentation</li>
<li><strong>Scroll Spy</strong>: Highlights current section as you scroll</li>
<li><strong>Smooth Navigation</strong>: Animated scrolling to clicked sections</li>
<li><strong>Responsive Design</strong>: Auto-hides on mobile devices</li>
</ul>
</div>
<h3 id="responsive">1.1.1 Responsive Behavior</h3>
<div class="content-section">
<p>The navigator intelligently adapts to different screen sizes. On desktop computers, it remains visible as a floating panel. On mobile devices, it automatically hides to preserve screen real estate for content.</p>
<p>Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).</p>
</div>
<h3 id="accessibility">1.1.2 Accessibility Features</h3>
<div class="content-section">
<p>The DocumentNavigator is built with accessibility in mind:</p>
<ul>
<li>Full keyboard navigation support</li>
<li>ARIA labels and proper semantic markup</li>
<li>Screen reader compatibility</li>
<li>High contrast hover states</li>
<li>Focus management</li>
</ul>
</div>
<h2 id="implementation">1.2 Implementation Details</h2>
<div class="content-section">
<p>The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.</p>
<p>Key implementation highlights include:</p>
<ul>
<li><code>extractHeadings()</code> - Scans DOM for heading elements</li>
<li><code>buildNavigationTree()</code> - Creates hierarchical structure</li>
<li><code>handleScroll()</code> - Manages scroll spy functionality</li>
<li><code>navigateToHeading()</code> - Handles smooth scrolling</li>
</ul>
</div>
<h1 id="architecture">2. Widget Architecture</h1>
<div class="content-section">
<p>The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.</p>
<p>The widget is designed as part of a larger plugin ecosystem that will allow developers to create custom UI components that can be loaded dynamically and configured independently.</p>
</div>
<h2 id="base-classes">2.1 Base Class Hierarchy</h2>
<div class="content-section">
<p>Our widget system is built on a foundation of base classes that provide common functionality:</p>
<ul>
<li><strong>Widget</strong>: Core functionality (events, state, lifecycle)</li>
<li><strong>UIWidget</strong>: DOM manipulation and visual behavior</li>
<li><strong>InteractiveWidget</strong>: Event handling and user interaction</li>
</ul>
<p>DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.</p>
</div>
<h3 id="events">2.1.1 Event System</h3>
<div class="content-section">
<p>The widget uses a custom event system built on the native EventTarget API. This allows for clean separation of concerns and easy integration with other components.</p>
<p>Key events emitted by DocumentNavigator:</p>
<ul>
<li><code>rendered</code> - Widget has been rendered to DOM</li>
<li><code>navigate</code> - User navigated to a heading</li>
<li><code>toggle</code> - Widget was expanded or collapsed</li>
<li><code>theme-changed</code> - Theme was changed</li>
<li><code>destroyed</code> - Widget was destroyed</li>
</ul>
</div>
<h3 id="state">2.1.2 State Management</h3>
<div class="content-section">
<p>State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.</p>
<p>This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.</p>
</div>
<h2 id="plugin-system">2.2 Plugin System Integration</h2>
<div class="content-section">
<p>While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:</p>
<ul>
<li>Metadata and versioning information</li>
<li>Dependency declarations</li>
<li>Default configuration options</li>
<li>Lifecycle hooks</li>
<li>Theme variants</li>
<li>Development helpers</li>
</ul>
</div>
<h1 id="usage">3. Usage Examples</h1>
<div class="content-section">
<p>The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.</p>
</div>
<h2 id="basic-usage">3.1 Basic Usage</h2>
<div class="content-section">
<p>The simplest way to use DocumentNavigator is with default settings:</p>
<pre><code>const navigator = new DocumentNavigator();
await navigator.initialize();
await navigator.render();</code></pre>
<p>This creates a navigator with default settings that will scan the entire document for headings and display them in a collapsible panel on the left side.</p>
</div>
<h2 id="advanced-usage">3.2 Advanced Configuration</h2>
<div class="content-section">
<p>For more control, you can specify detailed configuration options:</p>
<pre><code>const navigator = new DocumentNavigator({
position: 'right',
collapsed: false,
theme: 'dark',
maxHeadingLevel: 4,
enableScrollSpy: true,
smoothScroll: true
});</code></pre>
<p>This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.</p>
</div>
<h3 id="theming">3.2.1 Custom Theming</h3>
<div class="content-section">
<p>The navigator supports multiple built-in themes and can be extended with custom themes. The theming system integrates with MarkiTect's document themes for consistent styling.</p>
<p>Available themes include <code>default</code>, <code>dark</code>, and <code>minimal</code>, each optimized for different use cases and aesthetics.</p>
</div>
<h1 id="testing">4. Testing and Quality</h1>
<div class="content-section">
<p>The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.</p>
</div>
<h2 id="test-coverage">4.1 Test Coverage</h2>
<div class="content-section">
<p>Our test suite covers all major functionality:</p>
<ul>
<li>Widget instantiation and configuration</li>
<li>DOM rendering and element creation</li>
<li>Heading extraction and hierarchy building</li>
<li>Navigation and smooth scrolling</li>
<li>Expand/collapse animations</li>
<li>Scroll spy functionality</li>
<li>Responsive behavior</li>
<li>Keyboard navigation</li>
<li>Event emission</li>
<li>Edge cases and error handling</li>
</ul>
</div>
<h2 id="performance">4.2 Performance Considerations</h2>
<div class="content-section">
<p>The navigator is optimized for performance with several key strategies:</p>
<ul>
<li><strong>Throttled Scroll Events</strong>: Scroll spy updates are throttled to 100ms intervals</li>
<li><strong>Efficient DOM Queries</strong>: Heading extraction is done once and cached</li>
<li><strong>Conditional Rendering</strong>: Navigator only renders if minimum heading count is met</li>
<li><strong>Memory Management</strong>: Proper cleanup prevents memory leaks</li>
<li><strong>Responsive Loading</strong>: Navigator automatically hides on mobile to save resources</li>
</ul>
</div>
<h1 id="conclusion">5. Conclusion</h1>
<div class="content-section">
<p>The DocumentNavigator widget successfully brings Substack-style navigation to MarkiTect documents. It provides an intuitive, accessible, and performant way for users to navigate long-form content.</p>
<p>The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.</p>
<p><strong>Scroll back to the top and try the navigation features!</strong> The hamburger menu should be visible on the left side of your screen.</p>
</div>
</div>
<!-- Load widget classes -->
<script type="module">
// Import our widget classes
import { Widget } from '../widgets/base/Widget.js';
import { UIWidget } from '../widgets/base/UIWidget.js';
import { DocumentNavigator } from '../widgets/navigation/DocumentNavigator.js';
// Make classes available globally for demo
window.Widget = Widget;
window.UIWidget = UIWidget;
window.DocumentNavigator = DocumentNavigator;
// Initialize navigator on page load
document.addEventListener('DOMContentLoaded', async () => {
console.log('🧭 Initializing DocumentNavigator demo...');
try {
// Create navigator with demo settings
const navigator = new DocumentNavigator({
container: document.getElementById('markdown-content'),
position: 'left',
collapsed: true,
theme: 'default',
enableScrollSpy: true,
autoHide: true,
maxHeadingLevel: 3,
minHeadings: 1 // Show navigator even with few headings for demo
});
// Initialize and render
await navigator.initialize();
const element = await navigator.render();
if (element) {
console.log('✅ DocumentNavigator initialized successfully!');
console.log(` Found ${navigator.headings.length} headings`);
console.log(' Click the hamburger menu (☰) to expand navigation');
} else {
console.log(' DocumentNavigator not rendered (insufficient headings)');
}
// Add some debugging helpers
window.navigator = navigator;
window.testNavigator = {
expand: () => navigator.expand(),
collapse: () => navigator.collapse(),
toggle: () => navigator.toggle(),
showHeadings: () => console.table(navigator.headings),
showTree: () => console.log(navigator.navigationTree)
};
console.log('🔧 Debugging helpers available:');
console.log(' window.navigator - navigator instance');
console.log(' window.testNavigator - helper functions');
} catch (error) {
console.error('❌ DocumentNavigator initialization failed:', error);
}
});
</script>
</body>
</html>

View File

@@ -1,285 +0,0 @@
#!/usr/bin/env node
/**
* Real User Functionality Tests
*
* This test file validates the actual functionality that users experience,
* not just internal API calls. It tests the complete user workflow.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
runner.describe('Real User Functionality Tests', () => {
runner.it('should allow users to edit content and see changes in DOM', () => {
// Load all extracted components
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
const debugModule = require('../components/debug-panel.js');
const controlsModule = require('../components/document-controls.js');
const { SectionManager } = sectionModule;
const { DOMRenderer } = domModule;
const { DebugPanel } = debugModule;
const { DocumentControls } = controlsModule;
// Setup DOM container
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
// Create components
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
// Setup document controls
documentControls.create();
// Create sections from test markdown
const testMarkdown = `# Original Title\nOriginal content that should be editable.`;
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const firstSection = sections[0];
const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
// Verify original content is rendered
runner.expect(sectionElement.innerHTML).toContain('Original Title');
// Simulate user clicking on section
const clickEvent = new Event('click', { bubbles: true });
sectionElement.dispatchEvent(clickEvent);
// Verify editing state is active
runner.expect(firstSection.isEditing()).toBeTruthy();
// Find the floating menu and edit controls
const floatingMenu = document.querySelector('.ui-edit-floating-menu');
runner.expect(floatingMenu).toBeTruthy();
const textarea = floatingMenu.querySelector('textarea');
const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
runner.expect(textarea).toBeTruthy();
runner.expect(acceptButton).toBeTruthy();
// Simulate user editing content
const newContent = '# Updated Title\nCompletely new content added by user.';
textarea.value = newContent;
// Simulate user clicking accept
acceptButton.click();
// Verify section is no longer editing
runner.expect(firstSection.isEditing()).toBeFalsy();
// Verify floating menu is gone
const menuAfterAccept = document.querySelector('.ui-edit-floating-menu');
runner.expect(menuAfterAccept).toBeFalsy();
// CRITICAL TEST: Verify DOM was actually updated with new content
const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
runner.expect(updatedElement.innerHTML).toContain('Updated Title');
runner.expect(updatedElement.innerHTML).toContain('Completely new content');
runner.expect(updatedElement.innerHTML).not.toContain('Original Title');
// Cleanup
document.body.removeChild(container);
documentControls.destroy();
});
runner.it('should allow users to reset all changes', () => {
// Setup similar to above
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
const controlsModule = require('../components/document-controls.js');
const { SectionManager } = sectionModule;
const { DOMRenderer } = domModule;
const { DocumentControls } = controlsModule;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const documentControls = new DocumentControls();
documentControls.create();
// Create and modify content
const testMarkdown = `# Test Section\nOriginal content for reset test.`;
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const firstSection = sections[0];
// Make changes to the section
sectionManager.startEditing(firstSection.id);
sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.');
sectionManager.acceptChanges(firstSection.id);
// Verify changes are applied
let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
runner.expect(sectionElement.innerHTML).toContain('Modified Title');
runner.expect(firstSection.hasChanges()).toBeTruthy();
// Test reset functionality
const resetButton = documentControls.getButton('reset-all');
runner.expect(resetButton).toBeTruthy();
// Click reset button
resetButton.click();
// Verify content is reset
sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
runner.expect(sectionElement.innerHTML).toContain('Test Section');
runner.expect(sectionElement.innerHTML).not.toContain('Modified Title');
runner.expect(firstSection.hasChanges()).toBeFalsy();
// Cleanup
document.body.removeChild(container);
documentControls.destroy();
});
runner.it('should handle cancel operations correctly', () => {
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
const { SectionManager } = sectionModule;
const { DOMRenderer } = domModule;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`;
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
const firstSection = sections[0];
const originalContent = firstSection.currentMarkdown;
// Start editing
const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
sectionElement.click();
// Make changes but cancel them
const floatingMenu = document.querySelector('.ui-edit-floating-menu');
const textarea = floatingMenu.querySelector('textarea');
const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel'));
textarea.value = '# This should be cancelled\nThis content should not appear.';
cancelButton.click();
// Verify content is unchanged
const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
runner.expect(unchangedElement.innerHTML).toContain('Cancel Test');
runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled');
runner.expect(firstSection.currentMarkdown).toBe(originalContent);
// Cleanup
document.body.removeChild(container);
});
runner.it('should validate the complete editing workflow', () => {
// This test validates the entire user experience end-to-end
const sectionModule = require('../core/section-manager.js');
const domModule = require('../components/dom-renderer.js');
const debugModule = require('../components/debug-panel.js');
const controlsModule = require('../components/document-controls.js');
const { SectionManager } = sectionModule;
const { DOMRenderer } = domModule;
const { DebugPanel } = debugModule;
const { DocumentControls } = controlsModule;
const container = document.createElement('div');
container.innerHTML = '<div id="markdown-content"></div>';
document.body.appendChild(container);
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
documentControls.create();
// Multi-section document
const testMarkdown = `# Document Title
Introduction paragraph.
## Section A
Content for section A.
## Section B
Content for section B.`;
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
domRenderer.renderAllSections(sections);
// Verify all sections are rendered
const renderedSections = container.querySelectorAll('.ui-edit-section');
runner.expect(renderedSections.length).toBe(sections.length);
// Test editing multiple sections
const firstSection = sections[0];
const secondSection = sections[2]; // Section A
// Edit first section
renderedSections[0].click();
let floatingMenu = document.querySelector('.ui-edit-floating-menu');
let textarea = floatingMenu.querySelector('textarea');
let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
textarea.value = '# Updated Document Title\nUpdated introduction.';
acceptButton.click();
// Edit second section
renderedSections[2].click();
floatingMenu = document.querySelector('.ui-edit-floating-menu');
textarea = floatingMenu.querySelector('textarea');
acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
textarea.value = '## Updated Section A\nCompletely new content for section A.';
acceptButton.click();
// Verify both sections were updated
const updatedSections = container.querySelectorAll('.ui-edit-section');
runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title');
runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A');
// Test reset restores all sections
const resetButton = documentControls.getButton('reset-all');
resetButton.click();
const resetSections = container.querySelectorAll('.ui-edit-section');
runner.expect(resetSections[0].innerHTML).toContain('Document Title');
runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title');
runner.expect(resetSections[2].innerHTML).toContain('Section A');
runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A');
// Cleanup
document.body.removeChild(container);
documentControls.destroy();
});
});
module.exports = runner;
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Running Real User Functionality Tests');
runner.run().then(() => {
console.log('✅ Real user functionality tests completed');
console.log('These tests validate what users actually experience, not just internal APIs');
});
}

View File

@@ -1,196 +0,0 @@
#!/usr/bin/env node
/**
* TDD Test for SectionManager Component Extraction
*
* Tests the extraction of SectionManager from the monolithic editor.js
* Ensures all functionality is preserved during refactoring.
*/
const RefactorTestRunner = require('./refactor-test-runner.js');
const runner = new RefactorTestRunner();
// First, let's define what the SectionManager API should look like
const EXPECTED_SECTION_MANAGER_API = [
'constructor',
'createSectionsFromMarkdown',
'startEditing',
'stopEditing',
'getAllSections',
'sections', // Map property, not method
'getDocumentStatus',
'getDocumentMarkdown',
'on', // event system
'emit', // event system
'handleSectionSplit',
'updateContent',
'acceptChanges',
'cancelChanges',
'resetSection'
];
runner.describe('SectionManager Component Extraction', () => {
runner.it('should define expected API methods', () => {
// This test defines what we expect from the extracted SectionManager
const expectedMethods = EXPECTED_SECTION_MANAGER_API;
runner.expect(expectedMethods.length).toBe(15);
runner.expect(expectedMethods).toContain('createSectionsFromMarkdown');
runner.expect(expectedMethods).toContain('startEditing');
runner.expect(expectedMethods).toContain('stopEditing');
});
runner.it('should extract from monolithic editor.js', () => {
// Load the monolithic editor.js to extract SectionManager
delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
try {
const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
runner.expect(editorModule.SectionManager).toBeTruthy();
// Set global for other tests
global.SectionManager = editorModule.SectionManager;
global.Section = editorModule.Section;
global.EditState = editorModule.EditState;
} catch (error) {
throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
}
});
runner.it('should preserve SectionManager constructor functionality', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
runner.expect(manager).toBeInstanceOf(SectionManager);
runner.expect(manager.sections).toBeInstanceOf(Map);
});
runner.it('should preserve createSectionsFromMarkdown functionality', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
const sections = manager.createSectionsFromMarkdown(testMarkdown);
runner.expect(Array.isArray(sections)).toBeTruthy();
runner.expect(sections.length).toBe(2);
runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
});
runner.it('should preserve section editing state management', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
const sections = manager.createSectionsFromMarkdown('# Test\nContent');
const sectionId = sections[0].id;
// Test start editing
runner.expect(manager.startEditing(sectionId)).toBeTruthy();
const section = manager.sections.get(sectionId);
runner.expect(section.isEditing()).toBeTruthy();
// Test stop editing
section.stopEditing();
runner.expect(section.isEditing()).toBeFalsy();
});
runner.it('should preserve event system functionality', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
let eventFired = false;
let eventData = null;
manager.on('test-event', (data) => {
eventFired = true;
eventData = data;
});
manager.emit('test-event', { test: 'data' });
runner.expect(eventFired).toBeTruthy();
runner.expect(eventData).toEqual({ test: 'data' });
});
runner.it('should preserve document status functionality', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
manager.createSectionsFromMarkdown('# Test\nContent');
const status = manager.getDocumentStatus();
runner.expect(status).toHaveProperty('totalSections');
runner.expect(status).toHaveProperty('editingSections');
runner.expect(status.totalSections).toBe(1);
});
runner.it('should preserve getAllSections functionality', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
const testMarkdown = '# One\nContent\n\n# Two\nMore content';
manager.createSectionsFromMarkdown(testMarkdown);
const allSections = manager.getAllSections();
runner.expect(Array.isArray(allSections)).toBeTruthy();
runner.expect(allSections.length).toBe(2);
});
runner.it('should preserve section splitting functionality', () => {
const SectionManager = global.SectionManager;
const manager = new SectionManager();
const sections = manager.createSectionsFromMarkdown('# Original\nContent');
const sectionId = sections[0].id;
const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
const newSections = manager.handleSectionSplit(sectionId, newContent);
runner.expect(Array.isArray(newSections)).toBeTruthy();
runner.expect(newSections.length).toBe(2);
runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
});
});
// Export API tests for use during extraction
const SECTION_MANAGER_API_TESTS = [
(SectionManager) => {
const manager = new SectionManager();
if (!manager.sections || !(manager.sections instanceof Map)) {
throw new Error('sections property missing or not a Map');
}
},
(SectionManager) => {
const manager = new SectionManager();
if (typeof manager.createSectionsFromMarkdown !== 'function') {
throw new Error('createSectionsFromMarkdown method missing');
}
},
(SectionManager) => {
const manager = new SectionManager();
if (typeof manager.startEditing !== 'function') {
throw new Error('startEditing method missing');
}
},
(SectionManager) => {
const manager = new SectionManager();
if (typeof manager.stopEditing !== 'function') {
throw new Error('stopEditing method missing');
}
}
];
module.exports = {
runner,
EXPECTED_SECTION_MANAGER_API,
SECTION_MANAGER_API_TESTS
};
// Run tests if called directly
if (require.main === module) {
console.log('🧪 Testing SectionManager Component Extraction');
runner.run().then(() => {
console.log('✅ SectionManager extraction tests completed');
});
}

View File

@@ -1,6 +0,0 @@
# Test Document
This is a test document to check if UI controls appear in edit mode.
## Section 1
Some content here.

View File

@@ -1,149 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Markitect Markitect v0.8.1.dev24+gdbde13e03.d20251111">
<title>Test Document</title>
<!-- Base styling for document content -->
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
background-color: #ffffff;
}
/* Responsive design */
@media (max-width: 768px) {
body {
padding: 1rem;
font-size: 0.9rem;
}
}
/* Content styling */
h1, h2, h3, h4, h5, h6 {
color: #2c3e50;
margin-top: 2rem;
margin-bottom: 1rem;
}
h1 { font-size: 2.5em; border-bottom: 3px solid #3498db; padding-bottom: 0.5rem; }
h2 { font-size: 2em; border-bottom: 2px solid #3498db; padding-bottom: 0.3rem; }
h3 { font-size: 1.5em; color: #34495e; }
p {
margin-bottom: 1.2rem;
text-align: justify;
}
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
blockquote {
border-left: 4px solid #3498db;
margin: 1.5rem 0;
padding-left: 1rem;
color: #7f8c8d;
}
code {
background-color: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
pre {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
border: 1px solid #e9ecef;
}
img {
max-width: 100%;
height: auto;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
table {
border-collapse: collapse;
width: 100%;
margin: 1.5rem 0;
}
th, td {
border: 1px solid #dee2e6;
padding: 0.75rem;
text-align: left;
}
th {
background-color: #f8f9fa;
font-weight: 600;
}
/* Print styles */
@media print {
.control-panel {
display: none !important;
}
body {
font-size: 12pt;
line-height: 1.4;
}
}
</style>
<!-- Control system styles -->
<link rel="stylesheet" href="markitect/static/css/controls.css">
<!-- External dependencies -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
onerror="console.error('CDN library failed to load - network or firewall blocking marked.js'); window.markitectMarkedError = true;"></script>
</head>
<body>
<div id="markitect-content">
<h1 id="test-document">Test Document</h1>
<p>This is a test document to check if UI controls appear in edit mode.</p>
<h2 id="section-1">Section 1</h2>
<p>Some content here.</p>
<hr />
<p><em>-- html from markdown by <a href="https://coulomb.social/open/MarkiTect" target="_blank">MarkiTect</a> on 2025-11-11 23:42:23 by <a href="https://coulomb.social/open/worsch" target="_blank">worsch</a></em></p>
</div>
<!-- Core JavaScript modules -->
<script src="markitect/static/js/core/debug-system.js"></script>
<!-- Control system -->
<script src="../js/controls/control-base.js"></script>
<script src="../js/controls/status-control.js"></script>
<!-- Main application -->
<script src="markitect/static/js/main.js"></script>
<!-- Handle CDN loading errors -->
<script>
window.addEventListener('load', function() {
if (window.markitectMarkedError) {
console.error("CDN library failed to load - network or firewall blocking marked.js");
}
});
</script>
</body>
</html>

View File

@@ -1,215 +0,0 @@
/**
* UI Widget Base Class
*
* Extends Widget with DOM manipulation and visual functionality.
* Base for all widgets that render UI elements.
*/
import { Widget } from './Widget.js';
export class UIWidget extends Widget {
constructor(options = {}) {
super(options);
// UI properties
this.element = null;
this.isVisible = false;
this.isRendered = false;
this.theme = options.theme || 'default';
this.cssClasses = new Set(['markitect-widget']);
// Animation support
this.animationDuration = options.animationDuration || 300;
this.enableAnimations = options.enableAnimations !== false;
}
/**
* Render the widget to DOM (abstract method)
*/
async render() {
throw new Error('render() method must be implemented by subclass');
}
/**
* Show the widget
*/
async show(options = {}) {
if (!this.isRendered) {
await this.render();
}
if (this.isVisible) {
return this;
}
this.isVisible = true;
if (this.element) {
if (this.enableAnimations && !options.immediate) {
await this.animateShow();
} else {
this.element.style.display = '';
}
}
this.emit('shown');
return this;
}
/**
* Hide the widget
*/
async hide(options = {}) {
if (!this.isVisible) {
return this;
}
this.isVisible = false;
if (this.element) {
if (this.enableAnimations && !options.immediate) {
await this.animateHide();
} else {
this.element.style.display = 'none';
}
}
this.emit('hidden');
return this;
}
/**
* Toggle visibility
*/
async toggle(options = {}) {
return this.isVisible ? this.hide(options) : this.show(options);
}
/**
* Show animation (override for custom animations)
*/
async animateShow() {
if (!this.element) return;
return new Promise(resolve => {
this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
this.element.style.opacity = '0';
this.element.style.display = '';
// Force reflow
this.element.offsetHeight;
this.element.style.opacity = '1';
setTimeout(() => {
this.element.style.transition = '';
resolve();
}, this.animationDuration);
});
}
/**
* Hide animation (override for custom animations)
*/
async animateHide() {
if (!this.element) return;
return new Promise(resolve => {
this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
this.element.style.opacity = '0';
setTimeout(() => {
this.element.style.display = 'none';
this.element.style.transition = '';
this.element.style.opacity = '';
resolve();
}, this.animationDuration);
});
}
/**
* CSS class management
*/
addClass(className) {
this.cssClasses.add(className);
if (this.element) {
this.element.classList.add(className);
}
return this;
}
removeClass(className) {
this.cssClasses.delete(className);
if (this.element) {
this.element.classList.remove(className);
}
return this;
}
hasClass(className) {
return this.cssClasses.has(className);
}
/**
* Apply theme styling
*/
applyTheme(themeName) {
const oldTheme = this.theme;
this.theme = themeName;
this.removeClass(`theme-${oldTheme}`);
this.addClass(`theme-${themeName}`);
this.emit('theme-changed', { oldTheme, newTheme: themeName });
return this;
}
/**
* Find child element by selector
*/
findElement(selector) {
return this.element ? this.element.querySelector(selector) : null;
}
/**
* Find all child elements by selector
*/
findElements(selector) {
return this.element ? this.element.querySelectorAll(selector) : [];
}
/**
* Override destroy to clean up DOM
*/
async destroy() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
this.isRendered = false;
this.isVisible = false;
await super.destroy();
}
/**
* Apply all CSS classes to element
*/
applyCSSClasses(element = this.element) {
if (element) {
element.className = Array.from(this.cssClasses).join(' ');
}
}
/**
* Default configuration for UI widgets
*/
getDefaultConfig() {
return {
...super.getDefaultConfig(),
theme: 'default',
animationDuration: 300,
enableAnimations: true
};
}
}

View File

@@ -1,141 +0,0 @@
/**
* Base Widget Class
*
* Foundation class for all Markitect UI widgets following the plugin architecture.
* Provides core functionality for event handling, state management, and lifecycle.
*/
export class Widget extends EventTarget {
constructor(options = {}) {
super();
// Core properties
this.id = options.id || `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
this.container = options.container || document.body;
this.config = { ...this.getDefaultConfig(), ...options };
// State management
this.state = new Map();
this.isInitialized = false;
this.isDestroyed = false;
// Mixin support
this.mixins = [];
// Lifecycle hooks
this.onInitialize = options.onInitialize || (() => {});
this.onDestroy = options.onDestroy || (() => {});
}
/**
* Initialize the widget
*/
async initialize() {
if (this.isInitialized || this.isDestroyed) {
return this;
}
try {
await this.onInitialize(this);
this.isInitialized = true;
this.emit('initialized');
return this;
} catch (error) {
this.emit('error', { phase: 'initialize', error });
throw error;
}
}
/**
* Destroy the widget and clean up resources
*/
async destroy() {
if (this.isDestroyed) {
return;
}
try {
await this.onDestroy(this);
this.isDestroyed = true;
this.emit('destroyed');
} catch (error) {
this.emit('error', { phase: 'destroy', error });
throw error;
}
}
/**
* State management
*/
setState(key, value) {
const oldValue = this.state.get(key);
this.state.set(key, value);
this.emit('state-changed', { key, value, oldValue });
}
getState(key, defaultValue = null) {
return this.state.get(key) ?? defaultValue;
}
/**
* Event emission wrapper
*/
emit(eventType, data = {}) {
const event = new CustomEvent(eventType, {
detail: { widget: this, ...data }
});
this.dispatchEvent(event);
}
/**
* Apply mixin functionality
*/
applyMixin(mixin) {
if (typeof mixin === 'object') {
Object.assign(this, mixin);
this.mixins.push(mixin);
}
return this;
}
/**
* Default configuration (override in subclasses)
*/
getDefaultConfig() {
return {};
}
/**
* Utility method for creating DOM elements with styling
*/
createElement(tag, options = {}) {
const element = document.createElement(tag);
if (options.className) {
element.className = options.className;
}
if (options.textContent) {
element.textContent = options.textContent;
}
if (options.innerHTML) {
element.innerHTML = options.innerHTML;
}
if (options.style) {
if (typeof options.style === 'string') {
element.style.cssText = options.style;
} else {
Object.assign(element.style, options.style);
}
}
if (options.attributes) {
Object.entries(options.attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
}
return element;
}
}

View File

@@ -1,625 +0,0 @@
/**
* DocumentNavigator Widget
*
* Substack-style floating document navigation widget that displays a hierarchical
* table of contents based on document headings. Supports smooth scrolling,
* scroll spy, expand/collapse, and responsive behavior.
*/
import { UIWidget } from '../base/UIWidget.js';
export class DocumentNavigator extends UIWidget {
constructor(options = {}) {
super(options);
// Navigation state
this.isCollapsed = this.config.collapsed;
this.currentSection = null;
this.headings = [];
this.navigationTree = [];
// Scroll spy state
this.scrollSpyEnabled = this.config.enableScrollSpy;
this.scrollThrottle = null;
// Event bindings
this.boundScrollHandler = this.handleScroll.bind(this);
this.boundResizeHandler = this.handleResize.bind(this);
// Initialize responsive behavior
this.mediaQuery = window.matchMedia('(max-width: 768px)');
}
getDefaultConfig() {
return {
...super.getDefaultConfig(),
position: 'left', // 'left' or 'right'
collapsed: true, // Start collapsed
autoHide: true, // Hide on mobile
maxHeadingLevel: 3, // H1, H2, H3
enableScrollSpy: true, // Highlight current section
smoothScroll: true, // Smooth scroll behavior
animationDuration: 300, // Animation timing
minHeadings: 2, // Min headings to show navigator
theme: 'default', // Theme support
// Styling options
width: '280px',
collapsedWidth: '40px',
offset: { top: '80px', side: '20px' },
// Accessibility
enableKeyboard: true,
ariaLabel: 'Document Navigation'
};
}
async initialize() {
await super.initialize();
// Extract headings from container
this.extractHeadings();
this.buildNavigationTree();
// Set up event listeners
if (this.scrollSpyEnabled) {
window.addEventListener('scroll', this.boundScrollHandler, { passive: true });
}
if (this.config.autoHide) {
window.addEventListener('resize', this.boundResizeHandler);
this.handleResize(); // Initial check
}
return this;
}
async render() {
if (this.isRendered) {
return this.element;
}
// Check if we have enough headings
if (this.headings.length < this.config.minHeadings) {
this.isRendered = true;
return null; // Don't render if too few headings
}
// Create main container
this.element = this.createElement('nav', {
className: 'document-navigator markitect-widget',
attributes: {
'aria-label': this.config.ariaLabel,
'role': 'navigation'
},
style: this.getNavigatorStyle()
});
// Apply CSS classes
this.applyCSSClasses();
this.addClass('theme-' + this.theme);
this.addClass('position-' + this.config.position);
// Create toggle button (always visible)
this.createToggleButton();
// Create navigation list (hidden when collapsed)
this.createNavigationList();
// Set initial visibility state
if (this.isCollapsed) {
await this.collapse({ immediate: true });
} else {
await this.expand({ immediate: true });
}
// Append to container
this.container.appendChild(this.element);
// Initialize scroll spy
if (this.scrollSpyEnabled) {
this.updateCurrentSection();
}
this.isRendered = true;
this.emit('rendered');
return this.element;
}
createToggleButton() {
this.toggleButton = this.createElement('button', {
className: 'navigator-toggle',
attributes: {
'type': 'button',
'aria-label': this.isCollapsed ? 'Expand navigation' : 'Collapse navigation',
'aria-expanded': !this.isCollapsed
},
innerHTML: this.getToggleIcon(),
style: this.getToggleStyle()
});
// Toggle on click
this.toggleButton.addEventListener('click', async () => {
await this.toggle();
});
// Keyboard support
if (this.config.enableKeyboard) {
this.toggleButton.addEventListener('keydown', this.handleKeyboard.bind(this));
}
this.element.appendChild(this.toggleButton);
}
createNavigationList() {
this.navigationList = this.createElement('div', {
className: 'navigator-list',
style: this.getListStyle()
});
if (this.headings.length === 0) {
this.createEmptyState();
} else {
this.populateNavigationList();
}
this.element.appendChild(this.navigationList);
}
createEmptyState() {
const emptyMessage = this.createElement('div', {
className: 'navigator-empty',
textContent: 'No headings found',
style: {
padding: '1rem',
textAlign: 'center',
color: '#666',
fontStyle: 'italic'
}
});
this.navigationList.appendChild(emptyMessage);
}
populateNavigationList() {
// Create header
const header = this.createElement('div', {
className: 'navigator-header',
innerHTML: `
<h3>Contents</h3>
<button class="navigator-close" aria-label="Close navigation">✕</button>
`,
style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem 1rem 0.5rem',
borderBottom: '1px solid #eee',
marginBottom: '0.5rem'
}
});
// Close button functionality
const closeButton = header.querySelector('.navigator-close');
closeButton.addEventListener('click', async () => {
await this.collapse();
});
this.navigationList.appendChild(header);
// Create navigation items
const navContainer = this.createElement('div', {
className: 'navigator-items',
style: {
maxHeight: '70vh',
overflowY: 'auto',
padding: '0 0.5rem 1rem'
}
});
this.renderNavigationTree(navContainer, this.navigationTree);
this.navigationList.appendChild(navContainer);
}
renderNavigationTree(container, items, level = 0) {
items.forEach(item => {
const navItem = this.createElement('div', {
className: `navigator-item level-${level}`,
style: {
marginLeft: `${level * 1}rem`,
marginBottom: '0.25rem'
}
});
// Create clickable link
const link = this.createElement('a', {
className: 'navigator-link',
textContent: item.text,
attributes: {
'href': `#${item.id}`,
'data-target': item.id,
'data-level': item.level,
'role': 'button',
'tabindex': '0'
},
style: {
display: 'block',
padding: '0.5rem 0.75rem',
textDecoration: 'none',
color: '#333',
borderRadius: '4px',
fontSize: level === 0 ? '0.9rem' : '0.8rem',
fontWeight: level === 0 ? '600' : '400',
transition: 'all 0.2s ease',
cursor: 'pointer'
}
});
// Hover effects
link.addEventListener('mouseenter', () => {
link.style.backgroundColor = '#f0f0f0';
});
link.addEventListener('mouseleave', () => {
if (!link.classList.contains('active')) {
link.style.backgroundColor = '';
}
});
// Click navigation
link.addEventListener('click', (e) => {
e.preventDefault();
this.navigateToHeading(item.id);
});
navItem.appendChild(link);
// Render children recursively
if (item.children && item.children.length > 0) {
this.renderNavigationTree(navItem, item.children, level + 1);
}
container.appendChild(navItem);
});
}
extractHeadings() {
const headingSelectors = [];
for (let i = 1; i <= this.config.maxHeadingLevel; i++) {
headingSelectors.push(`h${i}`);
}
const headingElements = this.container.querySelectorAll(headingSelectors.join(', '));
this.headings = Array.from(headingElements).map((heading, index) => {
// Ensure heading has an ID
if (!heading.id) {
heading.id = `heading-${index + 1}`;
}
return {
element: heading,
id: heading.id,
text: heading.textContent.trim(),
level: parseInt(heading.tagName.substring(1)),
offset: heading.offsetTop
};
});
return this.headings;
}
buildNavigationTree() {
this.navigationTree = [];
const stack = [];
this.headings.forEach(heading => {
const item = {
...heading,
children: []
};
// Find correct parent based on heading level
while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
stack.pop();
}
if (stack.length === 0) {
// Top level item
this.navigationTree.push(item);
} else {
// Child item
stack[stack.length - 1].children.push(item);
}
stack.push(item);
});
return this.navigationTree;
}
async toggle(options = {}) {
return this.isCollapsed ? this.expand(options) : this.collapse(options);
}
async expand(options = {}) {
if (!this.isCollapsed) {
return this;
}
this.isCollapsed = false;
if (this.toggleButton) {
this.toggleButton.setAttribute('aria-expanded', 'true');
this.toggleButton.setAttribute('aria-label', 'Collapse navigation');
this.toggleButton.innerHTML = this.getToggleIcon();
}
if (this.navigationList) {
if (this.enableAnimations && !options.immediate) {
await this.animateExpand();
} else {
this.navigationList.style.display = '';
this.element.style.width = this.config.width;
}
}
this.emit('toggle', { expanded: true });
return this;
}
async collapse(options = {}) {
if (this.isCollapsed) {
return this;
}
this.isCollapsed = true;
if (this.toggleButton) {
this.toggleButton.setAttribute('aria-expanded', 'false');
this.toggleButton.setAttribute('aria-label', 'Expand navigation');
this.toggleButton.innerHTML = this.getToggleIcon();
}
if (this.navigationList) {
if (this.enableAnimations && !options.immediate) {
await this.animateCollapse();
} else {
this.navigationList.style.display = 'none';
this.element.style.width = this.config.collapsedWidth;
}
}
this.emit('toggle', { expanded: false });
return this;
}
async animateExpand() {
return new Promise(resolve => {
this.navigationList.style.opacity = '0';
this.navigationList.style.display = '';
// Animate width and opacity
this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`;
this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
// Force reflow
this.element.offsetWidth;
this.element.style.width = this.config.width;
this.navigationList.style.opacity = '1';
setTimeout(() => {
this.element.style.transition = '';
this.navigationList.style.transition = '';
resolve();
}, this.animationDuration);
});
}
async animateCollapse() {
return new Promise(resolve => {
this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`;
this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
this.navigationList.style.opacity = '0';
this.element.style.width = this.config.collapsedWidth;
setTimeout(() => {
this.navigationList.style.display = 'none';
this.element.style.transition = '';
this.navigationList.style.transition = '';
resolve();
}, this.animationDuration);
});
}
navigateToHeading(headingId) {
const targetElement = document.getElementById(headingId);
if (!targetElement) {
console.warn(`Heading with ID '${headingId}' not found`);
return;
}
// Update active navigation item
this.setActiveItem(headingId);
// Scroll to target
if (this.config.smoothScroll) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
} else {
targetElement.scrollIntoView();
}
// Emit navigation event
this.emit('navigate', { target: headingId, element: targetElement });
// Optionally collapse after navigation on mobile
if (this.mediaQuery.matches && this.config.autoHide) {
setTimeout(() => this.collapse(), 500);
}
}
setActiveItem(headingId) {
// Remove previous active state
const previousActive = this.findElement('.navigator-link.active');
if (previousActive) {
previousActive.classList.remove('active');
previousActive.style.backgroundColor = '';
}
// Set new active state
const newActive = this.findElement(`[data-target="${headingId}"]`);
if (newActive) {
newActive.classList.add('active');
newActive.style.backgroundColor = '#e3f2fd';
newActive.style.color = '#1976d2';
}
this.currentSection = headingId;
}
handleScroll() {
if (!this.scrollSpyEnabled || !this.isRendered) {
return;
}
// Throttle scroll events
if (this.scrollThrottle) {
return;
}
this.scrollThrottle = setTimeout(() => {
this.updateCurrentSection();
this.scrollThrottle = null;
}, 100);
}
updateCurrentSection() {
const scrollPosition = window.pageYOffset + 100; // Offset for header
let currentHeading = null;
// Find the current heading based on scroll position
for (let i = this.headings.length - 1; i >= 0; i--) {
const heading = this.headings[i];
if (heading.element.offsetTop <= scrollPosition) {
currentHeading = heading;
break;
}
}
if (currentHeading && currentHeading.id !== this.currentSection) {
this.setActiveItem(currentHeading.id);
}
}
getCurrentSection() {
return this.currentSection;
}
handleResize() {
if (!this.config.autoHide) {
return;
}
if (this.mediaQuery.matches) {
// Mobile: hide navigator
if (this.element) {
this.element.style.display = 'none';
}
} else {
// Desktop: show navigator
if (this.element) {
this.element.style.display = '';
}
}
}
handleKeyboard(event) {
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
this.toggle();
break;
case 'Escape':
event.preventDefault();
this.collapse();
break;
}
}
getNavigatorStyle() {
const baseStyle = {
position: 'fixed',
top: this.config.offset.top,
zIndex: '1000',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: '1px solid #e1e5e9',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
backdropFilter: 'blur(8px)',
width: this.isCollapsed ? this.config.collapsedWidth : this.config.width,
maxHeight: '80vh',
overflow: 'hidden',
transition: 'width 0.3s ease-in-out'
};
// Position-specific styling
if (this.config.position === 'left') {
baseStyle.left = this.config.offset.side;
} else {
baseStyle.right = this.config.offset.side;
}
return baseStyle;
}
getToggleStyle() {
return {
width: '100%',
height: this.config.collapsedWidth,
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
color: '#666',
transition: 'color 0.2s ease'
};
}
getListStyle() {
return {
display: this.isCollapsed ? 'none' : '',
opacity: this.isCollapsed ? '0' : '1'
};
}
getToggleIcon() {
if (this.isCollapsed) {
return this.config.position === 'left' ? '☰' : '☰';
} else {
return '✕';
}
}
async destroy() {
// Remove event listeners
window.removeEventListener('scroll', this.boundScrollHandler);
window.removeEventListener('resize', this.boundResizeHandler);
// Clear throttle
if (this.scrollThrottle) {
clearTimeout(this.scrollThrottle);
}
await super.destroy();
}
}