diff --git a/markitect/clean_document_manager.py b/markitect/clean_document_manager.py
new file mode 100644
index 00000000..85cb01f5
--- /dev/null
+++ b/markitect/clean_document_manager.py
@@ -0,0 +1,1430 @@
+"""
+Clean Document Manager - Simplified version with only clean editor support
+"""
+import json
+import re
+from pathlib import Path
+from typing import Dict, Any, Optional
+
+
+class CleanDocumentManager:
+ """
+ Simplified document manager that only supports the clean editor implementation.
+ All legacy code has been removed for clarity and maintainability.
+ """
+
+ def __init__(self, db_manager=None):
+ self.db_manager = db_manager
+
+ def render_file(self, input_file: str, output_file: str, template: str = None, css: str = None,
+ edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True) -> Dict[str, Any]:
+ """
+ Render a markdown file to HTML with optional clean editing capabilities.
+ """
+ input_path = Path(input_file)
+ output_path = Path(output_file)
+
+ if not input_path.exists():
+ raise FileNotFoundError(f"Input file not found: {input_file}")
+
+ # Read markdown content
+ markdown_content = input_path.read_text(encoding='utf-8')
+
+ # Extract title from markdown (first h1 heading)
+ title = self._extract_title_from_markdown(markdown_content)
+
+ # Get original filename without extension
+ original_filename = input_path.stem
+
+ # Get version information
+ version_info = self._get_version_info()
+
+ # Generate HTML content
+ html_content = self._generate_html_template(
+ markdown_content=markdown_content,
+ title=title,
+ css=css,
+ template=template,
+ edit_mode=edit_mode,
+ editor_theme=editor_theme,
+ keyboard_shortcuts=keyboard_shortcuts,
+ original_filename=original_filename,
+ version_info=version_info
+ )
+
+ # Write HTML file
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ output_path.write_text(html_content, encoding='utf-8')
+
+ return {
+ 'success': True,
+ 'input_file': str(input_path),
+ 'output_file': str(output_path),
+ 'edit_mode': edit_mode,
+ 'editor_theme': editor_theme
+ }
+
+ def _extract_title_from_markdown(self, markdown_content: str) -> str:
+ """Extract title from first h1 heading in markdown."""
+ match = re.search(r'^#\s+(.+)', markdown_content, re.MULTILINE)
+ if match:
+ return match.group(1).strip()
+ return "Markdown Document"
+
+ def _get_version_info(self) -> dict:
+ """Get repository name and version information."""
+ version_info = {
+ 'repo_name': 'Markitect',
+ 'version': '0.3.0',
+ 'git_info': ''
+ }
+
+ try:
+ # Try to get version from package metadata
+ from importlib.metadata import version as get_version
+ version_info['version'] = get_version('markitect')
+ except Exception:
+ pass
+
+ try:
+ # Try to get git information
+ import subprocess
+ from pathlib import Path
+
+ # Get git commit hash and status
+ try:
+ git_hash = subprocess.check_output(
+ ['git', 'rev-parse', '--short', 'HEAD'],
+ cwd=Path(__file__).parent,
+ stderr=subprocess.DEVNULL
+ ).decode().strip()
+
+ # Check if there are uncommitted changes
+ try:
+ subprocess.check_output(
+ ['git', 'diff-index', '--quiet', 'HEAD', '--'],
+ cwd=Path(__file__).parent,
+ stderr=subprocess.DEVNULL
+ )
+ git_status = ''
+ except subprocess.CalledProcessError:
+ git_status = '-modified'
+
+ version_info['git_info'] = f" (git:{git_hash}{git_status})"
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ pass
+ except Exception:
+ pass
+
+ return version_info
+
+ def _generate_html_template(self, markdown_content: str, title: str, css: str = None, template: str = None,
+ edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None) -> str:
+ """Generate clean HTML template."""
+
+ # Escape the markdown content for JavaScript
+ js_markdown_content = json.dumps(markdown_content)
+
+ # Handle CSS styles
+ css_content = ""
+ if css:
+ try:
+ css_path = Path(css)
+ if css_path.exists():
+ css_file_content = css_path.read_text(encoding='utf-8')
+ css_content = f""
+ else:
+ css_content = f''
+ except Exception:
+ css_content = f''
+
+ # Default CSS for basic styling
+ default_css = """
+
+ """
+
+ # Load clean editor JavaScript files
+ editor_scripts = ""
+ editor_config = ""
+ body_classes = ""
+
+ if edit_mode:
+ body_classes = ' class="markitect-edit-mode"'
+
+ # Configuration for clean editor
+ version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.3.0"
+ editor_config = f"""
+ const MARKITECT_EDIT_MODE = true;
+ const MARKITECT_EDITOR_CONFIG = {{
+ theme: '{editor_theme}',
+ keyboardShortcuts: {str(keyboard_shortcuts).lower()},
+ autosave: false,
+ sections: true,
+ originalFilename: '{original_filename}',
+ version: '{version_str}',
+ repoName: '{version_info['repo_name'] if version_info else 'Markitect'}'
+ }};
+
+ // Make config available globally
+ window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
+
+ # Load clean editor architecture
+ editor_scripts = self._get_clean_editor_scripts()
+
+ # Generate the complete HTML template
+ html_template = f"""
+
+
+
+
+ {title}
+ {css_content}
+ {default_css}
+
+
+
+
+
+
+
+
+
+"""
+
+ return html_template
+
+ def _get_clean_editor_scripts(self) -> str:
+ """Get the complete clean editor JavaScript code."""
+ return """
+ // Clean Editor Architecture
+ /**
+ * Test-Driven Section Editor Implementation
+ *
+ * A clean, object-oriented approach to handling section editing
+ * that can be tested independently of the DOM.
+ */
+
+// Enums for clear state management
+const EditState = Object.freeze({
+ ORIGINAL: 'original',
+ EDITING: 'editing',
+ MODIFIED: 'modified',
+ SAVED: 'saved'
+});
+
+const SectionType = Object.freeze({
+ HEADING: 'heading',
+ PARAGRAPH: 'paragraph',
+ LIST: 'list',
+ CODE: 'code',
+ BLOCKQUOTE: 'blockquote'
+});
+
+/**
+ * Section class - Core business logic for a single editable section
+ */
+class Section {
+ constructor(id, originalMarkdown, sectionType = SectionType.PARAGRAPH) {
+ this.id = id;
+ this.originalMarkdown = originalMarkdown;
+ this.currentMarkdown = originalMarkdown;
+ this.editingMarkdown = null;
+ this.pendingMarkdown = null;
+ this.sectionType = sectionType;
+ this.state = EditState.ORIGINAL;
+ this.domElement = null;
+ this.lastSaved = null;
+ this.created = new Date();
+ }
+
+ 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;
+ }
+ }
+
+ resetToOriginal() {
+ this.currentMarkdown = this.originalMarkdown;
+ this.editingMarkdown = null;
+ this.pendingMarkdown = null;
+ this.lastSaved = null;
+ this.state = EditState.ORIGINAL;
+ return this.originalMarkdown;
+ }
+
+ stopEditing() {
+ if (this.state !== EditState.EDITING) {
+ return this.state;
+ }
+
+ // If we have editing changes that differ from current content, preserve them as pending
+ if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
+ this.pendingMarkdown = this.editingMarkdown;
+ this.state = EditState.MODIFIED; // Has pending changes
+ } else {
+ // No changes made during this edit session
+ 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;
+ }
+
+ hasChanges() {
+ return this.currentMarkdown !== this.originalMarkdown;
+ }
+
+ isEditing() {
+ return this.state === EditState.EDITING;
+ }
+
+ getStatus() {
+ return {
+ id: this.id,
+ state: this.state,
+ hasChanges: this.hasChanges(),
+ isEditing: this.isEditing(),
+ contentLength: this.currentMarkdown.length,
+ lastSaved: this.lastSaved,
+ sectionType: this.sectionType
+ };
+ }
+
+ static generateId(content, position) {
+ const str = content.substring(0, 100) + position.toString();
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash;
+ }
+ return `section_${Math.abs(hash)}_${position}`;
+ }
+
+ static detectType(markdown) {
+ const trimmed = markdown.trim();
+ if (trimmed.startsWith('#')) return SectionType.HEADING;
+ if (trimmed.startsWith('```')) return SectionType.CODE;
+ if (trimmed.startsWith('>')) return SectionType.BLOCKQUOTE;
+ if (trimmed.startsWith('-') || trimmed.startsWith('*') || /^\\d+\\./.test(trimmed)) {
+ return SectionType.LIST;
+ }
+ return SectionType.PARAGRAPH;
+ }
+}
+
+/**
+ * SectionManager class - Manages the collection of sections
+ */
+class SectionManager {
+ constructor() {
+ this.sections = new Map();
+ // Note: Removed single editingSection tracking to allow multiple concurrent edits
+ this.listeners = new Map();
+ }
+
+ 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) {
+ const lines = markdownContent.split('\\n');
+ const sections = [];
+ let currentSection = '';
+ let position = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const isHeading = /^#{1,6}\\s/.test(line);
+ const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
+ const isNewSection = isHeading || isNewParagraph;
+
+ if (isNewSection && 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++;
+ currentSection = line;
+ } else {
+ if (currentSection) currentSection += '\\n';
+ currentSection += line;
+ }
+ }
+
+ 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);
+ }
+
+ this.emit('sections-created', { sections, count: sections.length });
+ return sections;
+ }
+
+ startEditing(sectionId) {
+ const section = this.sections.get(sectionId);
+ if (!section) {
+ throw new Error(`Section ${sectionId} not found`);
+ }
+
+ // Check if section is already being edited
+ if (section.isEditing()) {
+ console.log('Section already in editing state:', sectionId);
+ return section.editingMarkdown;
+ }
+
+ const content = section.startEdit();
+ // Note: No longer tracking single editingSection - allowing multiple
+ this.emit('edit-started', { sectionId, content, section: section.getStatus() });
+ return content;
+ }
+
+ updateContent(sectionId, markdown) {
+ const section = this.sections.get(sectionId);
+ if (!section) {
+ throw new Error(`Section ${sectionId} not found`);
+ }
+ section.updateContent(markdown);
+ this.emit('content-updated', { sectionId, markdown, section: section.getStatus() });
+ }
+
+ acceptChanges(sectionId) {
+ const section = this.sections.get(sectionId);
+ if (!section) {
+ throw new Error(`Section ${sectionId} not found`);
+ }
+
+ // Check if the edited content contains new headings that would create splits
+ const newContent = section.editingMarkdown;
+ const originalContent = section.originalMarkdown;
+ const shouldSplit = this.checkForSectionSplits(newContent, originalContent);
+
+ if (shouldSplit) {
+ // Handle section splitting
+ this.handleSectionSplit(sectionId, newContent);
+ } else {
+ // Normal accept without splitting
+ const content = section.acceptChanges();
+ // Note: No longer tracking single editingSection
+ this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
+ }
+
+ return section.currentMarkdown;
+ }
+
+ checkForSectionSplits(content, originalContent) {
+ if (!content) return false;
+
+ // Split by lines and check for headings
+ const lines = content.split('\\n');
+ const originalLines = originalContent ? originalContent.split('\\n') : [];
+
+ let newHeadingCount = 0;
+ let originalHeadingCount = 0;
+
+ // Count headings in new content
+ for (const line of lines) {
+ if (/^#{1,6}\\s/.test(line.trim())) {
+ newHeadingCount++;
+ }
+ }
+
+ // Count headings in original content
+ for (const line of originalLines) {
+ if (/^#{1,6}\\s/.test(line.trim())) {
+ originalHeadingCount++;
+ }
+ }
+
+ // Split if:
+ // 1. We have multiple headings now, OR
+ // 2. We added headings where there were none before, OR
+ // 3. We have more headings than we started with
+ return newHeadingCount > 1 ||
+ (originalHeadingCount === 0 && newHeadingCount > 0) ||
+ newHeadingCount > originalHeadingCount;
+ }
+
+ handleSectionSplit(originalSectionId, content) {
+ console.log('Splitting section:', originalSectionId);
+
+ const originalSection = this.sections.get(originalSectionId);
+ if (!originalSection) return;
+
+ // Accept the current changes first
+ originalSection.acceptChanges();
+
+ // Split the content into new sections
+ const newSections = this.createSectionsFromContent(content, originalSectionId);
+
+ // Get all sections as an ordered array to maintain document order
+ const allSectionsArray = Array.from(this.sections.values());
+ const originalIndex = allSectionsArray.findIndex(s => s.id === originalSectionId);
+
+ // Clear the sections map and rebuild it with proper order
+ this.sections.clear();
+
+ // Add sections before the original
+ for (let i = 0; i < originalIndex; i++) {
+ const section = allSectionsArray[i];
+ this.sections.set(section.id, section);
+ }
+
+ // Add the new split sections
+ newSections.forEach(section => {
+ this.sections.set(section.id, section);
+ });
+
+ // Add sections after the original
+ for (let i = originalIndex + 1; i < allSectionsArray.length; i++) {
+ const section = allSectionsArray[i];
+ this.sections.set(section.id, section);
+ }
+
+ // Note: No longer tracking single editingSection
+
+ // Emit event to trigger UI re-render
+ this.emit('section-split', {
+ originalSectionId,
+ newSections: newSections.map(s => s.getStatus()),
+ allSections: Array.from(this.sections.values())
+ });
+ }
+
+ createSectionsFromContent(content, baseSectionId) {
+ const lines = content.split('\\n');
+ const sections = [];
+ let currentSection = '';
+ let position = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const isHeading = /^#{1,6}\\s/.test(line.trim());
+
+ if (isHeading) {
+ // When we encounter a heading, complete any previous section
+ if (currentSection.trim()) {
+ const sectionId = `${baseSectionId}_split_${position}`;
+ const sectionType = Section.detectType(currentSection);
+ const section = new Section(sectionId, currentSection.trim(), sectionType);
+ sections.push(section);
+ position++;
+ }
+ // Start new section with this heading
+ currentSection = line;
+ } else {
+ // Add content to current section
+ if (currentSection) currentSection += '\\n';
+ currentSection += line;
+ }
+ }
+
+ // Add the final section if it has content
+ if (currentSection.trim()) {
+ const sectionId = `${baseSectionId}_split_${position}`;
+ const sectionType = Section.detectType(currentSection);
+ const section = new Section(sectionId, currentSection.trim(), sectionType);
+ sections.push(section);
+ }
+
+ return sections;
+ }
+
+ cancelChanges(sectionId) {
+ const section = this.sections.get(sectionId);
+ if (!section) {
+ throw new Error(`Section ${sectionId} not found`);
+ }
+ const content = section.cancelChanges();
+ // Note: No longer tracking single editingSection
+ this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
+ return content;
+ }
+
+ resetToOriginal(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;
+ }
+
+ stopEditing(sectionId) {
+ const section = this.sections.get(sectionId);
+ if (!section) {
+ throw new Error(`Section ${sectionId} not found`);
+ }
+
+ const newState = section.stopEditing();
+ // Note: No longer tracking single editingSection
+
+ this.emit('edit-stopped', { sectionId, newState, section: section.getStatus() });
+ return newState;
+ }
+
+ getAllSections() {
+ return Array.from(this.sections.values());
+ }
+
+ getDocumentMarkdown() {
+ return this.getAllSections()
+ .map(section => section.currentMarkdown)
+ .join('\\n\\n');
+ }
+}
+
+/**
+ * DOM Renderer - Handles DOM interactions
+ */
+class DOMRenderer {
+ constructor(sectionManager, containerElement) {
+ this.sectionManager = sectionManager;
+ this.container = containerElement;
+ // Note: Removed single currentSection tracking to allow multiple concurrent edits
+ this.editingSections = new Set(); // Track multiple editing sections
+
+ this.handleSectionClick = this.handleSectionClick.bind(this);
+ this.handleAccept = this.handleAccept.bind(this);
+ this.handleCancel = this.handleCancel.bind(this);
+ this.handleReset = this.handleReset.bind(this);
+ this.handleKeydown = this.handleKeydown.bind(this);
+
+ this.setupEventListeners();
+ }
+
+ setupEventListeners() {
+ this.sectionManager.on('sections-created', (data) => {
+ this.renderAllSections(data.sections);
+ });
+ this.sectionManager.on('edit-started', (data) => {
+ this.showEditor(data.sectionId, data.content);
+ });
+ this.sectionManager.on('edit-stopped', (data) => {
+ this.hideEditor(data.sectionId);
+ // Don't update content - let pending changes remain
+ });
+ this.sectionManager.on('changes-accepted', (data) => {
+ this.hideEditor(data.sectionId);
+ this.updateSectionContent(data.sectionId, data.content);
+ });
+ this.sectionManager.on('changes-cancelled', (data) => {
+ this.hideEditor(data.sectionId);
+ this.updateSectionContent(data.sectionId, data.content);
+ });
+ this.sectionManager.on('section-reset', (data) => {
+ this.updateTextareaContent(data.content, data.sectionId);
+ });
+ this.sectionManager.on('section-split', (data) => {
+ console.log('Handling section split in UI');
+ this.handleSectionSplit(data);
+ });
+ }
+
+ renderAllSections(sections) {
+ this.container.innerHTML = '';
+ sections.forEach(section => {
+ const element = this.createSectionElement(section);
+ section.domElement = element;
+ this.container.appendChild(element);
+ });
+ this.container.addEventListener('click', this.handleSectionClick);
+ }
+
+ createSectionElement(section) {
+ const element = document.createElement('div');
+ element.setAttribute('data-section-id', section.id);
+
+ if (typeof marked !== 'undefined') {
+ element.innerHTML = marked.parse(section.currentMarkdown);
+ } else {
+ element.innerHTML = `${section.currentMarkdown}
`;
+ }
+
+ // Setup styling and event handlers
+ this.setupSectionElement(element);
+
+ return element;
+ }
+
+ handleSectionClick(event) {
+ // Don't handle clicks on form elements or buttons
+ if (event.target.closest('textarea, button, input')) {
+ return;
+ }
+
+ const sectionElement = event.target.closest('.markitect-section-editable');
+ if (!sectionElement) return;
+
+ const sectionId = sectionElement.getAttribute('data-section-id');
+ if (!sectionId) return;
+
+ // Check if this section is already being edited
+ const section = this.sectionManager.sections.get(sectionId);
+ if (section && section.isEditing()) {
+ console.log('Section already being edited:', sectionId);
+ return;
+ }
+
+ try {
+ console.log('Starting edit for section:', sectionId);
+ this.sectionManager.startEditing(sectionId);
+ } catch (error) {
+ console.error('Failed to start editing:', error);
+ }
+ }
+
+ showEditor(sectionId, content) {
+ const element = this.findSectionElement(sectionId);
+ if (!element) return;
+
+ this.hideCurrentEditor();
+
+ const editorContainer = document.createElement('div');
+ editorContainer.style.cssText = `
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+ width: 100%;
+ `;
+
+ const textarea = document.createElement('textarea');
+ textarea.value = content;
+ textarea.style.cssText = `
+ flex: 1;
+ min-height: 100px;
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
+ border: 2px solid #007acc;
+ border-radius: 6px;
+ padding: 12px;
+ font-size: 14px;
+ line-height: 1.5;
+ resize: vertical;
+ `;
+
+ textarea.addEventListener('input', () => {
+ this.sectionManager.updateContent(sectionId, textarea.value);
+ });
+ textarea.addEventListener('keydown', this.handleKeydown);
+
+ const controls = document.createElement('div');
+ controls.style.cssText = `
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ `;
+
+ const createButton = (text, color, handler) => {
+ const btn = document.createElement('button');
+ btn.textContent = text;
+ btn.style.cssText = `
+ padding: 8px 12px;
+ border: none;
+ border-radius: 4px;
+ color: white;
+ background: ${color};
+ cursor: pointer;
+ font-size: 12px;
+ min-width: 70px;
+ `;
+ btn.addEventListener('click', handler);
+ return btn;
+ };
+
+ controls.appendChild(createButton('✓ Accept', '#4caf50', () => this.handleAccept(sectionId)));
+ controls.appendChild(createButton('✗ Cancel', '#f44336', () => this.handleCancel(sectionId)));
+ controls.appendChild(createButton('🔄 Reset', '#ff9800', () => this.handleReset(sectionId)));
+
+ editorContainer.appendChild(textarea);
+ editorContainer.appendChild(controls);
+
+ element.innerHTML = '';
+ element.appendChild(editorContainer);
+
+ textarea.focus();
+ // Track this section as being edited
+ this.editingSections.add(sectionId);
+ }
+
+ hideCurrentEditor() {
+ // This method is no longer needed since we support multiple editors
+ // Individual editors are hidden via hideEditor(sectionId)
+ }
+
+ hideEditor(sectionId) {
+ // Remove from editing sections set
+ this.editingSections.delete(sectionId);
+
+ // Force re-render the section to ensure it displays correctly
+ const section = this.sectionManager.sections.get(sectionId);
+ if (section) {
+ this.updateSectionContent(sectionId, section.currentMarkdown);
+ }
+ }
+
+ updateSectionContent(sectionId, content) {
+ const element = this.findSectionElement(sectionId);
+ if (!element) return;
+
+ if (typeof marked !== 'undefined') {
+ element.innerHTML = marked.parse(content);
+ } else {
+ element.innerHTML = `${content}
`;
+ }
+
+ // Restore the section styling and click behavior
+ this.setupSectionElement(element);
+ }
+
+ setupSectionElement(element) {
+ element.className = 'markitect-section-editable';
+ element.style.cssText = `
+ margin: 16px 0;
+ padding: 12px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border: 2px solid transparent;
+ `;
+
+ // Remove any existing event listeners to avoid duplicates
+ element.removeEventListener('mouseenter', element._mouseenterHandler);
+ element.removeEventListener('mouseleave', element._mouseleaveHandler);
+
+ // Create new handlers and store references
+ element._mouseenterHandler = () => {
+ element.style.backgroundColor = 'rgba(33, 150, 243, 0.05)';
+ element.style.borderColor = 'rgba(33, 150, 243, 0.2)';
+ };
+
+ element._mouseleaveHandler = () => {
+ element.style.backgroundColor = '';
+ element.style.borderColor = 'transparent';
+ };
+
+ element.addEventListener('mouseenter', element._mouseenterHandler);
+ element.addEventListener('mouseleave', element._mouseleaveHandler);
+ }
+
+ updateTextareaContent(content, sectionId) {
+ // Find the specific textarea for this section
+ const element = this.findSectionElement(sectionId);
+ if (element) {
+ const textarea = element.querySelector('textarea');
+ if (textarea) {
+ textarea.value = content;
+ }
+ }
+ }
+
+ handleSectionSplit(data) {
+ // Clear the editor state for the original section
+ this.editingSections.delete(data.originalSectionId);
+
+ // Find the original section element and its position
+ const originalElement = this.findSectionElement(data.originalSectionId);
+ if (!originalElement) {
+ console.error('Original section element not found');
+ return;
+ }
+
+ // Get the position of the original element
+ const originalPosition = Array.from(this.container.children).indexOf(originalElement);
+
+ // Create new section elements for the split sections
+ const newElements = [];
+ data.newSections.forEach(sectionData => {
+ const section = this.sectionManager.sections.get(sectionData.id);
+ if (section) {
+ const element = this.createSectionElement(section);
+ section.domElement = element;
+ newElements.push(element);
+ }
+ });
+
+ // Remove the original element
+ originalElement.remove();
+
+ // Insert new elements at the original position
+ if (originalPosition < this.container.children.length) {
+ const nextElement = this.container.children[originalPosition];
+ newElements.forEach(element => {
+ this.container.insertBefore(element, nextElement);
+ });
+ } else {
+ // If original was at the end, just append
+ newElements.forEach(element => {
+ this.container.appendChild(element);
+ });
+ }
+
+ // Show success message
+ console.log(`Section split into ${data.newSections.length} sections`);
+
+ // Notify the main editor about the split
+ if (window.markitectCleanEditor) {
+ window.markitectCleanEditor.showMessage(
+ `✂️ Section split into ${data.newSections.length} sections!`,
+ 'success'
+ );
+ }
+ }
+
+ findSectionElement(sectionId) {
+ return this.container.querySelector(`[data-section-id="${sectionId}"]`);
+ }
+
+ handleAccept(sectionId) {
+ try {
+ console.log('Accepting changes for section:', sectionId);
+ this.sectionManager.acceptChanges(sectionId);
+ console.log('Changes accepted successfully');
+ } catch (error) {
+ console.error('Failed to accept changes:', error);
+ }
+ }
+
+ handleCancel(sectionId) {
+ try {
+ console.log('Canceling changes for section:', sectionId);
+ this.sectionManager.cancelChanges(sectionId);
+ console.log('Changes canceled successfully');
+ } catch (error) {
+ console.error('Failed to cancel changes:', error);
+ }
+ }
+
+ handleReset(sectionId) {
+ try {
+ this.sectionManager.resetToOriginal(sectionId);
+ } catch (error) {
+ console.error('Failed to reset section:', error);
+ }
+ }
+
+ handleKeydown(event) {
+ if (!this.currentSection) return;
+ if (event.ctrlKey || event.metaKey) {
+ switch (event.key) {
+ case 'Enter':
+ event.preventDefault();
+ this.handleAccept(this.currentSection);
+ break;
+ case 'Escape':
+ event.preventDefault();
+ this.handleCancel(this.currentSection);
+ break;
+ }
+ }
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ this.handleCancel(this.currentSection);
+ }
+ }
+}
+
+/**
+ * Main Editor Integration
+ */
+class MarkitectCleanEditor {
+ constructor(markdownContent, containerElement, options = {}) {
+ this.options = {
+ theme: 'github',
+ keyboardShortcuts: true,
+ autosave: false,
+ ...options
+ };
+
+ this.sectionManager = new SectionManager();
+ this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
+ this.originalMarkdown = markdownContent;
+ this.initialize();
+ }
+
+ initialize() {
+ try {
+ const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
+ console.log(`✓ Initialized clean editor with ${sections.length} sections`);
+
+ // Add global control panel
+ this.addGlobalControls();
+
+ return true;
+ } catch (error) {
+ console.error('Failed to initialize clean editor:', error);
+ return false;
+ }
+ }
+
+ addGlobalControls() {
+ // Create floating control panel
+ const panel = document.createElement('div');
+ panel.id = 'markitect-global-controls';
+ panel.innerHTML = `
+
+
+
+
+
+
+ `;
+
+ // Style the panel
+ panel.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: white;
+ border: 2px solid #007acc;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ z-index: 1000;
+ font-family: system-ui, -apple-system, sans-serif;
+ min-width: 200px;
+ max-width: 250px;
+ `;
+
+ // Add internal styling
+ const style = document.createElement('style');
+ style.textContent = `
+ #markitect-global-controls .control-header h3 {
+ margin: 0 0 8px 0;
+ font-size: 16px;
+ color: #007acc;
+ }
+ #markitect-global-controls .control-status {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 12px;
+ }
+ #markitect-global-controls .control-btn {
+ display: block;
+ width: 100%;
+ margin: 6px 0;
+ padding: 10px 12px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s;
+ }
+ #markitect-global-controls .control-btn.primary {
+ background: #007acc;
+ color: white;
+ }
+ #markitect-global-controls .control-btn.primary:hover {
+ background: #005a9f;
+ }
+ #markitect-global-controls .control-btn.warning {
+ background: #ff9800;
+ color: white;
+ }
+ #markitect-global-controls .control-btn.warning:hover {
+ background: #f57c00;
+ }
+ #markitect-global-controls .control-btn.secondary {
+ background: #6c757d;
+ color: white;
+ }
+ #markitect-global-controls .control-btn.secondary:hover {
+ background: #545b62;
+ }
+ `;
+ document.head.appendChild(style);
+
+ document.body.appendChild(panel);
+
+ // Add event listeners
+ document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
+ document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
+ document.getElementById('show-status').addEventListener('click', () => this.showStatus());
+
+ // Update status periodically
+ this.statusInterval = setInterval(() => this.updateGlobalStatus(), 2000);
+ }
+
+ updateGlobalStatus() {
+ const statusEl = document.getElementById('editor-status');
+ if (!statusEl) return;
+
+ const sections = this.sectionManager.getAllSections();
+ const modified = sections.filter(s => s.hasChanges()).length;
+ const editing = sections.filter(s => s.isEditing()).length;
+
+ if (editing > 0) {
+ statusEl.textContent = `Editing ${editing} section${editing !== 1 ? 's' : ''}`;
+ statusEl.style.color = '#007acc';
+ } else if (modified > 0) {
+ statusEl.textContent = `${modified} section${modified !== 1 ? 's' : ''} modified`;
+ statusEl.style.color = '#ff9800';
+ } else {
+ statusEl.textContent = 'All sections saved';
+ statusEl.style.color = '#28a745';
+ }
+ }
+
+ saveDocument() {
+ const markdown = this.getDocumentMarkdown();
+ const blob = new Blob([markdown], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+
+ // Generate intelligent filename
+ const filename = this.generateSaveFilename();
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.style.display = 'none';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ console.log('📄 Document saved as:', filename);
+ this.showMessage(`Document saved as: ${filename}`, 'success');
+ }
+
+ generateSaveFilename() {
+ // Try to get original filename from config
+ let baseName = 'document';
+
+ // Method 1: Use original filename from config if available
+ if (typeof MARKITECT_EDITOR_CONFIG !== 'undefined' && MARKITECT_EDITOR_CONFIG.originalFilename) {
+ baseName = MARKITECT_EDITOR_CONFIG.originalFilename;
+ }
+
+ // Method 2: Try to extract from page title
+ if (baseName === 'document') {
+ const title = document.title;
+ if (title && title !== 'Markdown Document' && !title.includes('Debug') && !title.includes('Test')) {
+ baseName = title.toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special chars
+ .replace(/\s+/g, '-') // Replace spaces with dashes
+ .replace(/-+/g, '-') // Collapse multiple dashes
+ .replace(/^-|-$/g, ''); // Remove leading/trailing dashes
+ }
+ }
+
+ // Method 3: Try to extract from URL pathname
+ if (baseName === 'document') {
+ const urlPath = window.location.pathname;
+ const match = urlPath.match(/\/([^\/]+)\.html?$/);
+ if (match) {
+ const urlBaseName = match[1];
+ if (!urlBaseName.includes('debug') && !urlBaseName.includes('test')) {
+ baseName = urlBaseName.replace(/_/g, '-');
+ }
+ }
+ }
+
+ // Method 4: Try to extract from first heading
+ if (baseName === 'document') {
+ const firstHeading = this.sectionManager.getAllSections()
+ .find(section => section.sectionType === 'heading');
+ if (firstHeading) {
+ baseName = firstHeading.originalMarkdown
+ .replace(/^#+\s*/, '') // Remove markdown heading syntax
+ .toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, '')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-|-$/g, '')
+ .substring(0, 30); // Limit length
+ }
+ }
+
+ // Generate timestamp
+ const now = new Date();
+ const timestamp = now.toISOString()
+ .replace(/T/, '-')
+ .replace(/:/g, '-')
+ .replace(/\.\d{3}Z$/, '');
+
+ // Check if there are modifications
+ const hasModifications = this.sectionManager.getAllSections()
+ .some(section => section.hasChanges());
+
+ if (hasModifications) {
+ return `${baseName}-edited-${timestamp}.md`;
+ } else {
+ return `${baseName}-${timestamp}.md`;
+ }
+ }
+
+ resetAllSections() {
+ if (confirm('Reset all content to original markdown? This will lose all edits and remove split sections.')) {
+ // Clear the section manager completely
+ this.sectionManager.sections.clear();
+ // Note: No longer tracking single editingSection
+
+ // Recreate sections from original markdown
+ const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
+
+ console.log('🔄 All sections reset to original structure');
+ this.showMessage('Document reset to original structure', 'info');
+ }
+ }
+
+ showStatus() {
+ const sections = this.sectionManager.getAllSections();
+ const total = sections.length;
+ const modified = sections.filter(s => s.hasChanges()).length;
+ const editing = sections.filter(s => s.isEditing()).length;
+
+ // Get the actual save filename that will be used
+ const saveFilename = this.generateSaveFilename();
+
+ const message = `${window.editorConfig.repoName} ${window.editorConfig.version}
+Save file: ${saveFilename}
+Source: ${window.editorConfig.originalFilename}
+${window.location.protocol}//${window.location.host}${window.location.pathname}
+
+Document Status:
+• Total sections: ${total}
+• Modified sections: ${modified}
+• Currently editing: ${editing}
+• Unsaved changes: ${modified > 0 || editing > 0 ? 'Yes' : 'No'}
+
+SECTION BEHAVIOR:
+• Each section is a logical unit (heading + content until next heading)
+• Content with line breaks stays in one section
+• To split content: Create new headings (# ## ###)
+• Sections don't auto-split on line breaks
+
+EDITING CONTROLS:
+• Click any section to edit its content
+• Accept (✓) - Save changes to that section
+• Cancel (✗) - Discard changes, return to previous state
+• Reset (🔄) - Restore original content for that section
+• Save Document - Download all current content
+• Reset All - Restore entire document to original state`;
+
+ alert(message);
+ }
+
+ showMessage(message, type = 'info') {
+ const messageDiv = document.createElement('div');
+ messageDiv.textContent = message;
+
+ const colors = {
+ 'success': '#28a745',
+ 'error': '#dc3545',
+ 'info': '#007acc'
+ };
+
+ messageDiv.style.cssText = `
+ position: fixed;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: ${colors[type] || colors.info};
+ color: white;
+ padding: 12px 20px;
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+ z-index: 10001;
+ font-size: 14px;
+ max-width: 400px;
+ text-align: center;
+ `;
+
+ document.body.appendChild(messageDiv);
+
+ setTimeout(() => {
+ if (messageDiv.parentNode) {
+ messageDiv.parentNode.removeChild(messageDiv);
+ }
+ }, 3000);
+ }
+
+ getDocumentMarkdown() {
+ return this.sectionManager.getDocumentMarkdown();
+ }
+}
+
+// Initialize the clean editor system
+let markitectCleanEditor;
+
+function initializeCleanEditor() {
+ const container = document.getElementById('markdown-content');
+ if (!container) {
+ console.error('Markdown content container not found');
+ return;
+ }
+
+ if (typeof window.MarkitectEditor === 'undefined') {
+ console.error('MarkitectEditor not found');
+ return;
+ }
+
+ markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContent, container);
+ window.markitectCleanEditor = markitectCleanEditor; // Make globally available
+ console.log('✅ Clean section editor initialized successfully');
+}
+
+// Export for testing and usage
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
+} else {
+ window.MarkitectEditor = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
+}
+ """
\ No newline at end of file
diff --git a/markitect/cli.py b/markitect/cli.py
index e7a4d033..9ae11195 100644
--- a/markitect/cli.py
+++ b/markitect/cli.py
@@ -101,7 +101,7 @@ def detect_execution_mode():
def should_use_associated_files():
"""Determine if commands should use associated files behavior."""
return detect_execution_mode() == 'interactive'
-from .document_manager import DocumentManager
+# DocumentManager removed - using CleanDocumentManager directly in commands
from .serializer import ASTSerializer
from .cache_service import CacheDirectoryService
from .ast_service import ASTService
diff --git a/markitect/plugins/builtin/markdown_commands.py b/markitect/plugins/builtin/markdown_commands.py
index ca33248c..502e3fcd 100644
--- a/markitect/plugins/builtin/markdown_commands.py
+++ b/markitect/plugins/builtin/markdown_commands.py
@@ -16,7 +16,7 @@ from typing import Dict, Any
from markitect.plugins.base import CommandPlugin, PluginMetadata, PluginType
from markitect.plugins.decorators import register_plugin
-from markitect.document_manager import DocumentManager
+# DocumentManager removed - using CleanDocumentManager directly
from markitect.serializer import ASTSerializer
@@ -1659,7 +1659,7 @@ def md_list_command(ctx, output_format, names_only):
@click.option('--css', type=click.Path(),
help='Custom CSS file to include')
@click.option('--edit', is_flag=True,
- help='Open in live edit mode with preview')
+ help='Open in interactive edit mode with stable section editing')
@click.option('--editor-theme', default='github',
type=click.Choice(['github', 'monokai', 'tomorrow', 'dark']),
help='Editor theme for live edit mode (default: github)')
@@ -1704,17 +1704,20 @@ def md_render_command(ctx, input_file, output, template, css, edit, editor_theme
ensure_publication_directory(pub_dir)
output_path = pub_dir / get_output_filename(input_path)
- # Initialize document manager
- doc_manager = DocumentManager(config.get('db_manager'))
+ # Initialize clean document manager
+ from markitect.clean_document_manager import CleanDocumentManager
+ doc_manager = CleanDocumentManager(config.get('db_manager'))
# Render the file
if edit:
- # Live edit mode - generate HTML with editing capabilities
+ # Edit mode - generate HTML with editing capabilities
result = doc_manager.render_file(input_file, str(output_path),
template=template, css=css,
- edit_mode=True, editor_theme=editor_theme,
+ edit_mode=True,
+ editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts)
- click.echo(f"✓ Rendered with editing capabilities to: {output_path}")
+
+ click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
if config.get('verbose', False):
click.echo(f"Editor theme: {editor_theme}")