7 Commits

Author SHA1 Message Date
2e6f292e48 docs: Add design pattern examples and update submodule
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Add Design Pattern Documentation:
- Add CopyFirstMigration.md - Documents the copy-first migration principle
  used in the TestDrive-JSUI capability migration
- Add DontRepeatYourself.md - Documents the DRY principle
- Add DesignPrincipleSchema.json - JSON schema for design pattern documentation

Update Submodule:
- Update testdrive-jsui submodule pointer to include Phase 4 documentation
  (migration completion with legacy file cleanup)

Context:
These design pattern examples document the principles applied during the
successful TestDrive-JSUI migration, which serves as a reference implementation
of the copy-first migration pattern.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 17:00:31 +01:00
a1476a98b5 feat: update testdrive-jsui to v1.0.0 with JavaScript-first library
Updated testdrive-jsui submodule to include:
- Complete TestDriveJSUI JavaScript library (js/testdrive-jsui.js)
- Full editor example (examples/full-editor.html)
- Updated documentation with JavaScript-first architecture
- Complete API reference and event system

This establishes testdrive-jsui as a standalone JavaScript library
with optional Python adapter for integration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:15:08 +01:00
304959b3ee feat: add testdrive-jsui standalone proof of concept
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:06:57 +01:00
83086b3773 chore: update testdrive-jsui with architecture documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:04:20 +01:00
82eef76366 chore: cleanup post-migration artifacts
Removed empty legacy directories:
- markitect/static/js/ (empty after migration)
- testdrive-jsui/ (orphaned placeholder)

Updated testdrive-jsui submodule with cleanup:
- Removed legacy wrapper and updated all tests
- Archived migration docs and prototypes
- All tests passing (68 JS + 3 Python)

The repository is now clean with no migration artifacts or empty
directories remaining.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 11:43:52 +01:00
2838135450 chore: update testdrive-jsui submodule with documentation
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 11:10:57 +01:00
d592c5b8b3 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>
2025-12-16 10:27:05 +01:00
33 changed files with 454 additions and 13379 deletions

View File

@@ -0,0 +1,158 @@
# Design Principle: Copy First Migration
## Meta
- **Name:** Copy First Migration
- **ShortName:** CopyFirst
- **Version:** 0.1
- **Status:** Draft
- **Tags:** refactoring, migration, safety, testing, legacy
- **RelatedPrinciples:** Dont Repeat Yourself, Safe Refactoring, Test Pyramid, Capability-Based Testing
---
## Intent
Enable safe refactoring and structural migration of codebases by preserving
existing, working functionality until the new implementation is fully verified.
This principle prioritizes **reversibility, confidence, and continuity** over
speed or elegance.
---
## CoreStatement
Never move code directly; always copy first and delete only after verified
behavioral equivalence is established.
---
## Scope
### InScope
- Large-scale refactors or directory restructurings
- Technology or language migrations (e.g. JS → new JS layout, JS → Python integration)
- Legacy code stabilization
- Safety-critical or business-critical systems
- Situations with incomplete test coverage
### OutOfScope
- Greenfield development
- Trivial refactors with full and trusted test coverage
- One-off throwaway scripts
- Performance-driven rewrites where duplication is unacceptable
---
## InterpretationGuidelines
### What “Copy First” Means
- The original code remains untouched and functional
- The new version is treated as **experimental until proven**
- Deletion is a **final, explicit act**, not an implicit side effect
### Common Misinterpretations
- “This is inefficient because it duplicates code”
→ Duplication is intentional and temporary
- “Moving files is faster”
→ Speed is not the optimization target here
- “Tests alone are enough”
→ Tests are necessary but not sufficient without behavioral comparison
---
## DetectionHeuristics
### Structural Signals
- Files or modules being relocated across directories or packages
- Parallel implementations during migration
- Introduction of a new architectural boundary
### Semantic Signals
- Code paths that must remain behaviorally identical
- Business rules with high regression risk
- Legacy logic that is poorly documented but relied upon
### Change-Cost Signals
- Rollbacks are expensive or disruptive
- Failures would impact production or customers
- Migration spans multiple commits or teams
---
## DiagnosticQuestions
1. What breaks if this migration is wrong?
2. Do we have a known-good reference implementation?
3. Can both old and new code paths run in parallel?
4. How quickly can we revert if a defect is found?
5. What is the minimal proof of behavioral equivalence?
---
## RecommendedActions
### Low-Risk Actions
- Copy files to the new location instead of moving
- Preserve original imports and entry points
- Add logging or tracing for comparison
### Medium-Risk Actions
- Introduce dual-track execution (old + new)
- Add integration tests targeting both implementations
- Compare outputs, side effects, and error behavior
### High-Risk Actions
- Switch production usage to the new implementation
- Remove old code only after full verification
- Collapse duplicated paths once confidence is established
---
## AcceptanceCriteria
- Original code remains functional until final removal
- New code passes all existing tests
- New integration tests validate identical behavior
- Dual-track comparisons show no regressions
- Deletion of old code is deliberate and reversible up to the final step
---
## AntiPatterns
- Moving files directly without a fallback
- Refactoring and migration in a single irreversible step
- Deleting “unused” code before equivalence is proven
- Assuming test parity guarantees behavioral parity
- Big-bang migrations without rollback paths
---
## Tradeoffs
Applying Copy First Migration intentionally:
- Introduces temporary duplication
- Increases short-term codebase size
- Slows perceived progress
These costs are justified by dramatically reduced risk and higher confidence
during complex migrations.
---
## AgentUsage
### When to Apply This Lens
- During directory, module, or architecture migrations
- When refactoring legacy or poorly understood code
- When safety and uptime matter more than speed
- When rollback must remain possible at all times
### When to Suspend This Lens
- In greenfield projects
- When full test coverage and confidence already exist
- For trivial mechanical refactors
### Expected Agent Output
- Identification of migration boundaries
- Copy-first migration plan with explicit stages
- Test strategy (unit, integration, dual-track)
- Rollback points and deletion criteria
- Clear signal for when old code may be removed
xxx

View File

@@ -0,0 +1,135 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Schema for DesignPrinciples",
"description": "JSON schema describing the markdown structure of OperationalKnowledge DesignPrinciples",
"properties": {
"headings": {
"type": "object",
"description": "Document heading structure",
"properties": {
"level_1": {
"type": "array",
"description": "Headings at level 1",
"items": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"level": {
"type": "integer"
},
"position": {
"type": "integer"
}
},
"required": [
"content",
"level"
]
},
"minItems": 1,
"maxItems": 1
},
"level_2": {
"type": "array",
"description": "Headings at level 2",
"items": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"level": {
"type": "integer"
},
"position": {
"type": "integer"
}
},
"required": [
"content",
"level"
]
},
"minItems": 4,
"maxItems": 12
},
"level_3": {
"type": "array",
"description": "Headings at level 3",
"items": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"level": {
"type": "integer"
},
"position": {
"type": "integer"
}
},
"required": [
"content",
"level"
]
},
"minItems": 0,
"maxItems": 40
}
}
},
"paragraphs": {
"type": "array",
"description": "Text paragraphs",
"minItems": 8,
"maxItems": 120
},
"lists": {
"type": "array",
"description": "Lists (ordered and unordered)",
"minItems": 0,
"maxItems": 20
},
"emphasis": {
"type": "array",
"description": "Text emphasis (bold, italic)",
"minItems": 0,
"maxItems": 120
},
"metadata": {
"type": "object",
"description": "Document structure metadata",
"properties": {
"total_elements": {
"type": "integer",
"const": 115
},
"structure_types": {
"type": "array",
"items": {
"type": "string"
},
"description": "All structural element types found",
"const": [
"paragraph_close",
"heading_close",
"hr",
"bullet_list_open",
"paragraph_open",
"heading_open",
"ordered_list_open",
"ordered_list_close",
"inline",
"list_item_close",
"list_item_open",
"bullet_list_close"
]
}
}
}
}
}

View File

@@ -0,0 +1,160 @@
# Design Principle: Dont Repeat Yourself (DRY)
## Meta
- **Name:** Dont Repeat Yourself
- **ShortName:** DRY
- **Version:** 0.1
- **Status:** Stable
- **Tags:** maintainability, refactoring, architecture, quality
- **RelatedPrinciples:** Single Responsibility, YAGNI, Separation of Concerns
---
## Intent
Reduce maintenance cost and behavioral drift by ensuring that each piece of
knowledge, rule, or decision logic has a single authoritative representation
in the codebase.
---
## CoreStatement
A codebase violates DRY when the same knowledge is expressed in multiple places
such that a change would require edits in more than one location or risks
inconsistent behavior.
---
## Scope
### InScope
- Business rules and decision logic
- Algorithms and validation logic
- Data schemas, DTOs, and field definitions
- Configuration values and feature flags
- Repeated workflows or orchestration logic
- Test setup and invariant test scenarios
### OutOfScope
- Superficial textual similarity without shared meaning
- Intentional duplication for isolation or clarity
- Early-stage exploratory code where abstractions are not yet clear
- Performance-driven duplication with explicit justification
---
## InterpretationGuidelines
### What “Repeat” Means
DRY is about **duplication of knowledge**, not duplication of text.
Examples of knowledge duplication:
- The same validation rule implemented in multiple services
- Identical conditional logic controlling the same behavior
- The same data structure defined independently in multiple modules
### Common Misinterpretations
- “Any repeated code is bad” (false)
- “DRY means maximum abstraction” (false)
- “Utility modules automatically improve DRY” (often false)
---
## DetectionHeuristics
### Structural Signals
- Functions with highly similar bodies and signatures
- Repeated constants, strings, regexes, or SQL fragments
- Parallel modules with mirrored internal structure
### Semantic Signals
- Identical error messages or validation rules in different layers
- Repeated mapping logic between the same concepts
- Copy-paste variations differing only in naming
### Change-Cost Signals
- A requirement change touches multiple files for the same reason
- Fixes applied in one location but missing in others
- Tests failing inconsistently after partial updates
---
## DiagnosticQuestions
1. Is this duplication representing the same rule or policy?
2. If this rule changes, how many places must be updated?
3. Is the duplicated logic stable or likely to evolve?
4. Are the differences intentional or accidental?
5. Where is the natural “source of truth” for this knowledge?
6. Would abstraction reduce or increase cognitive load?
---
## RecommendedActions
### Low-Risk Refactors
- Extract constants or configuration values
- Centralize literals and error messages
- Introduce shared test fixtures or helpers
### Medium-Risk Refactors
- Extract pure helper functions
- Introduce shared domain services or modules
- Unify schema/type definitions
### High-Risk Refactors
- Introduce strategy/template patterns
- Merge parallel subsystems
- Redesign domain boundaries to align ownership of rules
---
## AcceptanceCriteria
- Each rule or behavior has a single authoritative implementation
- Required changes affect fewer locations than before
- Naming reflects domain meaning, not technical convenience
- Tests pass without behavior regression
- Coupling does not increase unintentionally
---
## AntiPatterns
- “God” utility modules with unrelated helpers
- Over-generalized abstractions with many parameters
- Shared code across domains that should evolve independently
- Premature abstraction of coincidental similarities
- Hiding meaningful differences behind generic interfaces
---
## Tradeoffs
Applying DRY may:
- Increase indirection
- Reduce local readability
- Introduce coupling between modules
These costs are acceptable only when outweighed by reduced change cost
and increased behavioral consistency.
---
## AgentUsage
### When to Apply This Lens
- During refactoring or maintenance work
- When change requests repeatedly touch similar code
- When bugs recur due to partial updates
- During architectural consolidation
### When to Suspend This Lens
- During early exploration or prototyping
- When future variability is unclear
- When isolation is more valuable than reuse
### Expected Agent Output
- Identified DRY violations with locations
- Rationale for why duplication matters
- Volatility assessment (stable vs evolving)
- Recommended refactor type and target
- Risk notes and minimal patch sequence
xxx

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();
}
}