Files
markitect-main/markitect/document_manager.py
tegwick d0abaab63a
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 / code-quality (push) Has been cancelled
Test Suite / security-scan (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 / test-summary (push) Has been cancelled
chore: update project state and prepare for image support development
- Add comprehensive image test document with various image types
- Update project structure with development artifacts
- Prepare foundation for image support enhancement phase
- Include test files for validating image editing workflows

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 08:06:22 +01:00

2076 lines
84 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Document manager for high-performance markdown file ingestion and AST caching.
This module implements the core functionality for Issue #2: Fast Document Loading & CLI Manipulation.
It provides performance-optimized document processing through AST caching and database integration.
Key Features:
- Parse once, access many times architecture
- AST cache loading < 50% of markdown parsing time
- Seamless integration with Issue #1 database foundation
- Comprehensive error handling and validation
"""
import json
import time
from pathlib import Path
from typing import Dict, Any, Optional
from .parser import parse_markdown_to_ast
from .frontmatter import FrontMatterParser
class DocumentManager:
"""
High-performance document manager for markdown file processing.
Implements the "parse once, manipulate many times" architecture by creating
fast-loading AST cache files alongside database metadata storage.
Architecture:
markdown file → AST parsing → cache file + database metadata
Performance Goal:
Cache loading must be < 50% of original parsing time
Attributes:
db_manager: Database manager for metadata storage
cache_dir: Directory for AST cache files
frontmatter_parser: YAML front matter processor
"""
def __init__(self, database_manager, cache_dir: Optional[Path] = None):
"""
Initialize document manager with database and cache configuration.
Args:
database_manager: DatabaseManager instance for metadata storage
cache_dir: Directory for AST cache files (default: .ast_cache)
"""
self.db_manager = database_manager
self.cache_dir = Path(cache_dir) if cache_dir else Path(".ast_cache")
self.cache_dir.mkdir(exist_ok=True)
self.frontmatter_parser = FrontMatterParser()
def ingest_file(self, file_path: Path) -> Dict[str, Any]:
"""
Ingest a markdown file with performance-optimized AST caching.
Implements the core "parse once, manipulate many times" workflow:
1. Validates file existence
2. Parses markdown content to AST
3. Creates fast-loading AST cache file
4. Stores metadata in database
5. Returns processing results with performance metrics
Args:
file_path: Path to markdown file to ingest
Returns:
Dictionary containing:
- ast: Parsed AST representation
- metadata: File metadata (filename, title, etc.)
- ast_cache_path: Path to created cache file
- parse_time: Time spent parsing markdown (seconds)
- cache_time: Time spent creating cache (seconds)
Raises:
FileNotFoundError: If the specified file doesn't exist
Performance:
Initial parse creates overhead, but subsequent cache loads
will be < 50% of this parse time.
"""
# Validate file exists
if not file_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
# Read file content
content = self._read_file_content(file_path)
# Parse front matter for metadata extraction
front_matter, markdown_content = self.frontmatter_parser.parse(content)
# Parse to AST with performance timing
ast, parse_time = self._parse_content_to_ast(content)
# Create cache file with performance timing
cache_file, cache_time = self._create_performance_cache(file_path.name, ast)
# Store in database (handles front matter parsing internally)
self._store_in_database(file_path.name, content)
# Return comprehensive result
return self._build_ingestion_result(
ast=ast,
filename=file_path.name,
front_matter=front_matter,
cache_file=cache_file,
parse_time=parse_time,
cache_time=cache_time
)
def _read_file_content(self, file_path: Path) -> str:
"""
Read file content with proper encoding.
Args:
file_path: Path to file to read
Returns:
File content as string
"""
return file_path.read_text(encoding='utf-8')
def _parse_content_to_ast(self, content: str) -> tuple[list, float]:
"""
Parse markdown content to AST with performance timing.
Args:
content: Raw markdown content
Returns:
Tuple of (AST tokens, parse_time_seconds)
"""
start_time = time.time()
ast = parse_markdown_to_ast(content)
parse_time = time.time() - start_time
return ast, parse_time
def _create_performance_cache(self, filename: str, ast: list) -> tuple[Path, float]:
"""
Create AST cache file with performance timing.
Args:
filename: Source filename for cache naming
ast: AST tokens to cache
Returns:
Tuple of (cache_file_path, cache_time_seconds)
"""
start_time = time.time()
cache_file = self._create_ast_cache(filename, ast)
cache_time = time.time() - start_time
return cache_file, cache_time
def _store_in_database(self, filename: str, content: str) -> None:
"""
Store document in database using existing API.
Args:
filename: Name of the file
content: Full markdown content (including front matter)
Note:
The database manager handles front matter parsing internally.
"""
self.db_manager.store_markdown_file(filename, content)
def _build_ingestion_result(self, ast: list, filename: str, front_matter: dict,
cache_file: Path, parse_time: float, cache_time: float) -> Dict[str, Any]:
"""
Build comprehensive ingestion result dictionary.
Args:
ast: Parsed AST tokens
filename: Source filename
front_matter: Parsed front matter metadata
cache_file: Path to created cache file
parse_time: Time spent parsing (seconds)
cache_time: Time spent caching (seconds)
Returns:
Structured result dictionary with all ingestion data
"""
return {
'ast': ast,
'metadata': {
'filename': filename,
'title': front_matter.get('title', ''),
},
'ast_cache_path': cache_file,
'parse_time': parse_time,
'cache_time': cache_time
}
def _create_ast_cache(self, filename: str, ast: list) -> Path:
"""
Create AST cache file in JSON format.
Args:
filename: Source filename for cache naming
ast: AST tokens to serialize
Returns:
Path to created cache file
"""
cache_filename = f"{filename}.ast.json"
cache_path = self.cache_dir / cache_filename
with open(cache_path, 'w', encoding='utf-8') as f:
json.dump(ast, f, indent=2, ensure_ascii=False)
return cache_path
def list_files(self) -> list:
"""
List all markdown files in the system.
Returns:
List of dictionaries containing file metadata including filename,
size, and modification date information.
"""
# Get files from database
db_files = self.db_manager.list_markdown_files()
# Enhance with file system information
enhanced_files = []
for file_info in db_files:
enhanced_info = {
'filename': file_info['filename'],
'id': file_info['id'],
'created_at': file_info['created_at'],
'front_matter': file_info['front_matter']
}
# Try to get file system stats if file exists
try:
file_path = Path(file_info['filename'])
if file_path.exists():
stat = file_path.stat()
enhanced_info['size'] = f"{stat.st_size} bytes"
enhanced_info['modified'] = stat.st_mtime
else:
enhanced_info['size'] = 'unknown'
enhanced_info['modified'] = 'file not found'
except Exception:
enhanced_info['size'] = 'unknown'
enhanced_info['modified'] = 'unknown'
enhanced_files.append(enhanced_info)
return enhanced_files
def get_file(self, file_path: str) -> Dict[str, Any]:
"""
Retrieve a markdown file from the database.
Args:
file_path: Path to the markdown file to retrieve
Returns:
Dictionary containing file content and metadata
Raises:
FileNotFoundError: If file is not found in database
"""
if not self.db_manager:
raise ValueError("Database manager not initialized")
# Get file from database
file_data = self.db_manager.get_markdown_file(file_path)
if file_data is None:
raise FileNotFoundError(f"File '{file_path}' not found in database")
return {
'content': file_data.get('content', ''),
'metadata': {
'filename': file_data.get('filename', file_path),
'front_matter': file_data.get('front_matter'),
'size': len(file_data.get('content', '')),
'modified': file_data.get('modified')
}
}
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 client-side rendering capabilities.
Creates an HTML file with embedded markdown content that is rendered
client-side using JavaScript markdown parser.
Args:
input_file: Path to input markdown file
output_file: Path to output HTML file
template: Template to use (optional)
css: CSS file to include (optional)
edit_mode: Enable interactive edit mode (default: False)
editor_theme: Editor theme (default: 'github')
keyboard_shortcuts: Enable keyboard shortcuts (default: True)
Returns:
Dictionary with rendering results and metadata
Raises:
FileNotFoundError: If input file doesn't exist
"""
import json
input_path = Path(input_file)
output_path = Path(output_file)
# Validate input file exists
if not input_path.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
# 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)
# 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
)
# Write HTML file
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(html_content, encoding='utf-8')
return {
'input_file': str(input_path),
'output_file': str(output_path),
'title': title,
'template': template,
'css': css
}
def _extract_title_from_markdown(self, content: str) -> str:
"""Extract title from markdown content (first h1 heading)."""
import re
# Look for first h1 heading
match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
if match:
return match.group(1).strip()
return "Markdown Document"
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) -> str:
"""Generate HTML template with embedded markdown and client-side rendering."""
import json
from pathlib import Path
# Escape the markdown content for JavaScript
js_markdown_content = json.dumps(markdown_content)
# Clean mode only - no utility functions needed
# Handle CSS styles
css_content = ""
if css:
# Try to read CSS file content and embed it
try:
css_path = Path(css)
if css_path.exists():
css_file_content = css_path.read_text(encoding='utf-8')
css_content = f"<style>\n{css_file_content}\n</style>"
else:
# Fallback to link if file doesn't exist
css_content = f'<link rel="stylesheet" href="{css}">'
except Exception:
# Fallback to link on any error
css_content = f'<link rel="stylesheet" href="{css}">'
# Get template-specific CSS
template_css = self._get_template_css(template)
# Default CSS for basic styling
default_css = f"""
<style>
{template_css}
</style>
"""
# Add editor-specific content if in edit mode
editor_scripts = ""
editor_config = ""
editor_css = ""
body_classes = ""
if edit_mode:
body_classes = ' class="markitect-edit-mode"'
if edit_type == 'clean':
# Load clean editor architecture
editor_css = ""
else:
# Legacy editor CSS
editor_css = """
<style>
/* Floating Control Panel - Slide-in from right */
.markitect-control-panel {
position: fixed;
top: 20px;
right: -320px;
width: 320px;
background: rgba(255, 255, 255, 0.98);
border: 1px solid #e0e0e0;
border-radius: 12px 0 0 12px;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
backdrop-filter: blur(10px);
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.markitect-control-panel.expanded {
right: 0;
}
.markitect-control-panel.expanded .markitect-control-ribbon {
opacity: 0;
pointer-events: none;
}
/* Control ribbon - always visible */
.markitect-control-ribbon {
position: absolute;
left: -40px;
top: 0;
width: 40px;
height: 100%;
background: linear-gradient(135deg, #2196f3, #1976d2);
border-radius: 8px 0 0 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
transition: all 0.3s ease, opacity 0.3s ease;
opacity: 1;
pointer-events: auto;
}
.markitect-control-ribbon:hover {
background: linear-gradient(135deg, #1976d2, #1565c0);
transform: translateX(-2px);
}
/* Panel content */
.markitect-panel-header {
background: linear-gradient(135deg, #2196f3, #1976d2);
color: white;
padding: 16px 20px;
border-radius: 12px 0 0 0;
position: relative;
}
.markitect-panel-title {
font-weight: 600;
font-size: 16px;
margin: 0 0 4px 0;
}
.markitect-panel-version {
font-size: 12px;
opacity: 0.9;
margin: 0;
}
.markitect-panel-close {
position: absolute;
top: 12px;
right: 16px;
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 20px;
padding: 4px;
border-radius: 4px;
transition: background 0.2s;
}
.markitect-panel-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.markitect-panel-body {
padding: 20px;
}
.markitect-status-section {
margin-bottom: 20px;
}
.markitect-status-indicator {
display: flex;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 12px;
border-left: 4px solid #4caf50;
}
.markitect-status-indicator.loading {
border-left-color: #ff9800;
}
.markitect-status-indicator.error {
border-left-color: #f44336;
background: #ffebee;
}
.markitect-status-icon {
margin-right: 8px;
font-size: 16px;
}
.markitect-status-text {
flex: 1;
font-size: 14px;
margin: 0;
}
.markitect-controls-section {
border-top: 1px solid #e0e0e0;
padding-top: 20px;
}
.markitect-controls-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.markitect-control-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
background: #2196f3;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
text-decoration: none;
}
.markitect-control-btn:hover {
background: #1976d2;
transform: translateY(-1px);
}
.markitect-control-btn.secondary {
background: #757575;
}
.markitect-control-btn.secondary:hover {
background: #616161;
}
.markitect-control-btn .icon {
margin-right: 6px;
}
.markitect-save-info {
font-size: 12px;
color: #666;
background: #f5f5f5;
padding: 8px 12px;
border-radius: 6px;
margin-top: 10px;
}
/* Visual indicators for section status */
.markitect-section-editable:not([data-edited])::before {
content: "📄";
position: absolute;
top: -2px;
right: -2px;
font-size: 12px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.markitect-section-editable[data-edited]::before {
content: "✏️";
position: absolute;
top: -2px;
right: -2px;
font-size: 12px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.markitect-section-editable:hover::before {
opacity: 0.7;
}
.markitect-error-details {
display: none;
background: #ffebee;
border: 1px solid #f44336;
border-radius: 8px;
padding: 12px;
margin-top: 12px;
}
.markitect-error-title {
font-weight: bold;
color: #c62828;
margin: 0 0 8px 0;
}
.markitect-error-text {
color: #666;
font-size: 14px;
margin: 0 0 8px 0;
}
.markitect-error-help {
font-size: 12px;
color: #666;
}
/* Content editing styles */
.markitect-section-editable {
border: 1px dashed transparent;
padding: 8px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.markitect-section-editable:hover {
border-color: #007acc;
background: rgba(0, 122, 204, 0.05);
}
.markitect-section-editable[data-edited] {
border-color: rgba(76, 175, 80, 0.3);
background: rgba(76, 175, 80, 0.02);
}
.markitect-section-editable[data-edited]:hover {
border-color: #4caf50;
background: rgba(76, 175, 80, 0.08);
}
.edit-mode textarea {
width: 100%; /* Fill the textarea wrapper */
min-height: 60px;
max-height: 360px;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
border: 2px solid #007acc;
border-radius: 6px;
padding: 12px;
font-size: inherit; /* Will be overridden by JavaScript */
line-height: inherit; /* Will be overridden by JavaScript */
resize: vertical; /* Allow only vertical resize to prevent layout issues */
overflow: auto;
box-sizing: border-box;
transition: height 0.15s ease;
min-width: 400px; /* Reasonable minimum width */
z-index: 100; /* Ensure it appears above other content */
position: relative;
}
.edit-mode textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
/* Edit section layout container */
.markitect-edit-container {
display: flex;
gap: 12px;
align-items: flex-start;
width: 100%;
}
.markitect-textarea-wrapper {
flex: 1;
min-width: 0; /* Allow textarea to shrink */
}
/* Section-level control buttons - vertical column on the right */
.markitect-section-controls {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 80px;
flex-shrink: 0;
}
.markitect-section-btn {
padding: 8px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
min-height: 32px;
}
.markitect-section-btn.accept {
background: #4caf50;
color: white;
}
.markitect-section-btn.accept:hover {
background: #45a049;
}
.markitect-section-btn.cancel {
background: #f44336;
color: white;
}
.markitect-section-btn.cancel:hover {
background: #da190b;
}
.markitect-section-btn.reset {
background: #ff9800;
color: white;
}
.markitect-section-btn.reset:hover {
background: #f57c00;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.markitect-control-panel {
width: 280px;
right: -280px;
}
.markitect-edit-container {
flex-direction: column;
gap: 8px;
}
.markitect-section-controls {
flex-direction: row;
justify-content: center;
min-width: auto;
}
.edit-mode textarea {
min-width: 100%;
}
}
@media (max-width: 480px) {
.markitect-section-controls {
flex-wrap: wrap;
gap: 4px;
}
.markitect-section-btn {
flex: 1;
min-width: 70px;
}
}
</style>"""
if edit_type == 'clean':
# Load the clean editor architecture
try:
with open('/home/worsch/markitect_project/src/section_editor.js', 'r') as f:
section_editor_js = f.read()
with open('/home/worsch/markitect_project/src/dom_renderer.js', 'r') as f:
dom_renderer_js = f.read()
with open('/home/worsch/markitect_project/src/clean_editor_integration.js', 'r') as f:
clean_integration_js = f.read()
except FileNotFoundError as e:
print(f"Warning: Clean editor files not found: {e}")
section_editor_js = "// Clean editor files not found"
dom_renderer_js = ""
clean_integration_js = ""
# Escape the markdown content for JavaScript
escaped_markdown = markdown_content.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
editor_config = f"""
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: false,
sections: true
}};
// Clean Editor Architecture
{section_editor_js}
{dom_renderer_js}
{clean_integration_js}
// 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;
}}
const markdownContent = `{escaped_markdown}`;
// Create the clean editor
markitectCleanEditor = new MarkitectEditor.MarkitectCleanEditor(markdownContent, container, {{
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: false
}});
// Add control panel
markitectCleanEditor.addControlPanel();
console.log('✅ Clean section editor initialized successfully');
}}
function getCleanEditorMarkdown() {{
return markitectCleanEditor ? markitectCleanEditor.getDocumentMarkdown() : '';
}}
function resetAllSections() {{
if (markitectCleanEditor) {{
markitectCleanEditor.resetAllSections();
}}
}}"""
else:
# Legacy editor configuration
editor_config = f"""
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: false,
sections: true
}};"""
if edit_type == 'clean':
# Clean editor uses minimal scripts since functionality is in the config
editor_scripts = """
// Clean editor initialization handled in editor_config above
// No additional scripts needed"""
else:
# Legacy editor scripts
editor_scripts = """
// Legacy editor scripts
// All functionality provided by the legacy editor system
initializeEditor() {
// Control panel is already in HTML, just make content editable
this.makeContentEditable();
// Auto-expand control panel briefly to show it's available
setTimeout(() => {
const panel = document.getElementById('markitect-control-panel');
if (panel) {
panel.classList.add('expanded');
setTimeout(() => {
panel.classList.remove('expanded');
}, 2000); // Show for 2 seconds then minimize
}
}, 1000);
}
makeContentEditable() {
const content = document.getElementById('markdown-content');
if (content) {
content.addEventListener('click', this.handleSectionClick.bind(this));
content.addEventListener('contextmenu', this.handleSectionContextMenu.bind(this));
this.markSections(content);
}
}
markSections(element) {
// Clear existing section markers (except edited ones)
const existingSections = element.querySelectorAll('.markitect-section-editable:not([data-edited])');
existingSections.forEach(section => {
section.classList.remove('markitect-section-editable');
section.removeAttribute('data-section');
});
// Mark new sections (skip elements inside edited wrappers)
const sections = element.querySelectorAll('h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol');
sections.forEach((section, index) => {
// Skip if this element is inside an edited wrapper
if (section.closest('[data-edited]')) {
return;
}
// Skip if already marked as edited wrapper
if (section.hasAttribute('data-edited')) {
return;
}
section.classList.add('markitect-section-editable');
// Use stable section ID based on content hash and position to prevent re-indexing issues
const stableSectionId = this.generateStableSectionId(section, index);
section.setAttribute('data-section', stableSectionId);
// Store original markdown for this specific section if not already stored
if (!this.originalMarkdownMap.has(stableSectionId)) {
const originalMarkdown = this.extractOriginalMarkdownForElement(section, index);
if (originalMarkdown) {
this.originalMarkdownMap.set(stableSectionId, originalMarkdown);
console.log(`📝 Stored original markdown for section ${stableSectionId}: "${originalMarkdown.substring(0, 50)}..."`);
}
}
});
}
generateStableSectionId(element, index) {
// Generate a stable section ID that won't change when sections are re-marked
const elementType = element.tagName.toLowerCase();
const elementText = element.textContent.trim().substring(0, 50);
// Create a simple hash from element content and type
let hash = 0;
const str = elementType + elementText + index;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return `section_${Math.abs(hash)}_${index}`;
}
extractOriginalMarkdownForElement(element, sectionIndex) {
// Try to extract original markdown content for a specific rendered element
// by matching it to the original markdown content structure
try {
const elementType = element.tagName.toLowerCase();
const elementText = element.textContent.trim();
// Parse original markdown to find matching content
const lines = markdownContent.split('\\n');
if (elementType.startsWith('h')) {
// For headings, find matching heading text
const headingLevel = parseInt(elementType.charAt(1));
const headingPrefix = '#'.repeat(headingLevel) + ' ';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith(headingPrefix) && line.substring(headingPrefix.length).trim() === elementText) {
return line;
}
}
} else if (elementType === 'p') {
// For paragraphs, find matching paragraph content
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip headings and empty lines
if (!line || line.startsWith('#')) continue;
// Check if this line matches the paragraph text (allowing for markdown formatting)
const cleanLine = line.replace(/\\*\\*(.*?)\\*\\*/g, '$1').replace(/\\*(.*?)\\*/g, '$1').replace(/`(.*?)`/g, '$1').trim();
if (cleanLine === elementText || line === elementText) {
return line;
}
}
}
// Fallback: try to find any line that contains the element text
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line && line.includes(elementText)) {
return line;
}
}
return null;
} catch (error) {
console.warn(`Failed to extract original markdown for section ${sectionIndex}:`, error);
return null;
}
}
handleSectionClick(event) {
const section = event.target.closest('.markitect-section-editable');
if (section && !section.querySelector('textarea')) {
// First, close any other open textareas to prevent content bleeding
this.closeAllTextareas();
this.editSection(section);
}
}
closeAllTextareas() {
// Find and properly close all open textareas, preserving their content
const allTextareas = document.querySelectorAll('.markitect-section-editable textarea');
console.log(`🔍 Found ${allTextareas.length} open textareas to close while preserving content`);
allTextareas.forEach((textarea, index) => {
const parentSection = textarea.closest('.markitect-section-editable');
const sectionId = parentSection ? parentSection.getAttribute('data-section') : 'unknown';
console.log(`🔄 Closing textarea ${index} (section ${sectionId}) while preserving content`);
// Preserve the textarea content instead of canceling
if (parentSection && sectionId) {
this.preserveSectionEdit(parentSection, sectionId, textarea);
}
});
}
preserveSectionEdit(section, sectionId, textarea) {
// Preserve textarea content as a temporary edit without full save process
console.log(`💾 Preserving edit for section ${sectionId}`);
const content = textarea.value.trim();
if (!content) {
// If textarea is empty, restore original content
this.cancelSectionEditSilent(section, sectionId);
return;
}
// Store the current edit state temporarily
if (!this.tempEditMap) {
this.tempEditMap = new Map();
}
this.tempEditMap.set(sectionId, content);
// Create a preview wrapper showing the current edit
const wrapper = document.createElement('div');
wrapper.innerHTML = marked.parse(content);
wrapper.classList.add('markitect-section-editable');
wrapper.setAttribute('data-section', sectionId);
wrapper.setAttribute('data-temp-edit', 'true'); // Mark as temporary edit
wrapper.style.backgroundColor = 'rgba(255, 235, 59, 0.1)'; // Light yellow background to indicate edit
section.parentNode.replaceChild(wrapper, section);
console.log(`💾 Preserved edit for section ${sectionId}: "${content.substring(0, 50)}${content.length > 50 ? '...' : ''}"`);
}
cancelSectionEditSilent(section, sectionId) {
// Cancel editing and restore original content without triggering markSections
console.log(`🔇 Silently canceling edit for section ${sectionId}`);
const originalMarkdown = this.originalMarkdownMap.get(sectionId);
if (originalMarkdown) {
// Restore to original markdown content
const wrapper = document.createElement('div');
wrapper.innerHTML = marked.parse(originalMarkdown);
wrapper.classList.add('markitect-section-editable');
wrapper.setAttribute('data-section', sectionId);
// Remove data-edited to show it's back to original
wrapper.removeAttribute('data-edited');
section.parentNode.replaceChild(wrapper, section);
console.log(`🔇 Silently restored section ${sectionId} to original content`);
} else {
console.warn(`⚠️ No original markdown found for section ${sectionId}, removing section`);
// Fallback: just remove the section if we can't restore it
if (section.parentNode) {
section.parentNode.removeChild(section);
}
}
}
saveTextareaContent(textarea) {
// Manually trigger the save logic that would normally happen on blur
const parentSection = textarea.closest('.markitect-section-editable');
if (!parentSection) return;
const content = textarea.value.trim();
const paragraphs = content.split(/\\n\\s*\\n/).filter(p => p.trim());
const sectionId = parentSection.getAttribute('data-section');
console.log(`💾 Manually saving content for section ${sectionId}: "${content}"`);
if (paragraphs.length > 1) {
// Multiple paragraphs - create separate sections
const nextSibling = parentSection.nextSibling;
parentSection.parentNode.removeChild(parentSection);
paragraphs.forEach((paragraph, index) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = marked.parse(paragraph.trim());
wrapper.classList.add('markitect-section-editable');
wrapper.setAttribute('data-section', sectionId + '_split_' + index);
wrapper.setAttribute('data-edited', 'true');
parentSection.parentNode.insertBefore(wrapper, nextSibling);
});
} else {
// Single content block
const wrapper = document.createElement('div');
wrapper.innerHTML = marked.parse(content);
wrapper.classList.add('markitect-section-editable');
wrapper.setAttribute('data-section', sectionId);
wrapper.setAttribute('data-edited', 'true');
parentSection.parentNode.replaceChild(wrapper, parentSection);
}
this.hasEdits = true;
// DON'T call markSections here - it causes re-indexing and content bleeding
// this.markSections(document.getElementById('markdown-content'));
this.updateSaveStatus();
}
handleSectionContextMenu(event) {
const section = event.target.closest('.markitect-section-editable');
if (section) {
event.preventDefault();
const sectionId = section.getAttribute('data-section');
const originalMarkdown = this.originalMarkdownMap.get(sectionId);
if (originalMarkdown) {
const menu = [
'Reset this section to original?',
'',
'Original content:',
originalMarkdown.length > 50 ? originalMarkdown.substring(0, 50) + '...' : originalMarkdown
].join('\\n');
if (confirm(menu)) {
this.resetSectionToOriginal(sectionId);
}
}
}
}
editSection(section) {
const sectionId = section.getAttribute('data-section');
console.log(`📝 Starting edit for section ${sectionId}`);
// Create a completely fresh textarea
const textarea = document.createElement('textarea');
textarea.className = 'edit-mode';
// Check for temporary edits first, then original markdown, then HTML conversion
const originalMarkdown = this.originalMarkdownMap.get(sectionId);
const tempEdit = this.tempEditMap ? this.tempEditMap.get(sectionId) : null;
const isEditedSection = section.hasAttribute('data-edited');
const isTempEdit = section.hasAttribute('data-temp-edit');
if (tempEdit && isTempEdit) {
// Restore temporary edit content
textarea.value = tempEdit;
console.log(`🔄 Restoring temporary edit for section ${sectionId}: "${tempEdit.substring(0, 100)}${tempEdit.length > 100 ? '...' : ''}"`);
} else if (originalMarkdown) {
// Use original markdown when available (for both edited and unedited sections)
textarea.value = originalMarkdown;
console.log(`🔄 Using original markdown for section ${sectionId}: "${originalMarkdown.substring(0, 100)}${originalMarkdown.length > 100 ? '...' : ''}"`);
} else {
// For sections without original markdown, convert from current HTML
const currentHTML = section.innerHTML;
const convertedContent = this.htmlToMarkdown(currentHTML);
textarea.value = convertedContent;
console.log(`⚠️ Converting from current HTML for section ${sectionId} (edited: ${isEditedSection}): "${convertedContent.substring(0, 100)}${convertedContent.length > 100 ? '...' : ''}"`);
console.log(` Source HTML was: "${currentHTML.substring(0, 100)}${currentHTML.length > 100 ? '...' : ''}"`);
}
// Ensure textarea value is properly set and prevent any bleeding
const finalValue = textarea.value || '';
textarea.value = finalValue;
textarea.defaultValue = finalValue;
console.log(`✅ Final textarea value for section ${sectionId}: "${finalValue.substring(0, 100)}${finalValue.length > 100 ? '...' : ''}"`);
// Verify no other textareas exist
const existingTextareas = document.querySelectorAll('.markitect-section-editable textarea');
if (existingTextareas.length > 0) {
console.warn(`⚠️ Found ${existingTextareas.length} existing textareas when starting new edit!`);
}
// Get original element font size and style
const computedStyle = window.getComputedStyle(section);
const originalFontSize = computedStyle.fontSize;
const originalLineHeight = computedStyle.lineHeight;
// Apply matching font size to textarea
textarea.style.fontSize = originalFontSize;
if (originalLineHeight !== 'normal') {
textarea.style.lineHeight = originalLineHeight;
}
// Auto-sizing function
const autoResize = () => {
// Temporarily disable transition for accurate measurement
const transition = textarea.style.transition;
textarea.style.transition = 'none';
// Reset height to measure scrollHeight
textarea.style.height = 'auto';
// Calculate based on actual content with more reasonable constraints
const contentHeight = textarea.scrollHeight;
const padding = 24; // 12px top + 12px bottom
// More reasonable sizing: min 2 lines, max 15 lines
const lineCount = textarea.value.split('\\n').length;
const minHeight = Math.max(60, lineCount * 24 + padding); // ~24px per line
const maxHeight = 360; // Maximum height constraint
const newHeight = Math.max(60, Math.min(maxHeight, Math.max(minHeight, contentHeight + 4)));
textarea.style.height = newHeight + 'px';
// Re-enable transition
textarea.style.transition = transition;
};
// Auto-resize on input and paste
textarea.addEventListener('input', autoResize);
textarea.addEventListener('paste', () => setTimeout(autoResize, 10));
// Initial sizing after DOM update
setTimeout(autoResize, 20);
// Note: Removed automatic blur handler that was causing content bleeding
// Content saving is now handled explicitly through the Accept button
// Create section controls
const controls = document.createElement('div');
controls.className = 'markitect-section-controls';
const acceptBtn = document.createElement('button');
acceptBtn.className = 'markitect-section-btn accept';
acceptBtn.innerHTML = '<span>✓</span> Accept';
acceptBtn.title = 'Accept changes and save this section';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'markitect-section-btn cancel';
cancelBtn.innerHTML = '<span>✗</span> Cancel';
cancelBtn.title = 'Cancel editing and revert to original';
const resetBtn = document.createElement('button');
resetBtn.className = 'markitect-section-btn reset';
resetBtn.innerHTML = '<span>🔄</span> Reset';
resetBtn.title = 'Reset to original markdown content';
// Add event listeners
acceptBtn.addEventListener('click', () => {
this.acceptSectionEdit(section, textarea);
});
cancelBtn.addEventListener('click', () => {
this.cancelSectionEdit(section, sectionId);
});
resetBtn.addEventListener('click', () => {
this.resetSectionEdit(section, sectionId, textarea);
});
controls.appendChild(acceptBtn);
controls.appendChild(cancelBtn);
controls.appendChild(resetBtn);
// Create the new layout structure
const editContainer = document.createElement('div');
editContainer.className = 'markitect-edit-container';
const textareaWrapper = document.createElement('div');
textareaWrapper.className = 'markitect-textarea-wrapper';
textareaWrapper.appendChild(textarea);
editContainer.appendChild(textareaWrapper);
editContainer.appendChild(controls);
// Completely clear the section and replace with the new layout
section.innerHTML = '';
section.appendChild(editContainer);
// Focus and ensure cursor is at start
textarea.focus();
textarea.setSelectionRange(0, 0);
}
htmlToMarkdown(html) {
// Create a temporary element to parse the HTML
const temp = document.createElement('div');
temp.innerHTML = html;
// Better HTML to Markdown conversion that preserves structure
let markdown = '';
const processNode = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
switch (tagName) {
case 'h1': return '# ' + node.textContent.trim();
case 'h2': return '## ' + node.textContent.trim();
case 'h3': return '### ' + node.textContent.trim();
case 'h4': return '#### ' + node.textContent.trim();
case 'h5': return '##### ' + node.textContent.trim();
case 'h6': return '###### ' + node.textContent.trim();
case 'p':
// Handle paragraphs with potential inline formatting
const childText = Array.from(node.childNodes).map(processNode).join('');
return childText;
case 'strong': case 'b':
return '**' + node.textContent + '**';
case 'em': case 'i':
return '*' + node.textContent + '*';
case 'code':
return '`' + node.textContent + '`';
case 'pre':
// Handle code blocks
const codeContent = node.textContent;
return '```\\n' + codeContent + '\\n```';
case 'blockquote':
const quoteLines = node.textContent.split('\\n');
return quoteLines.map(line => '> ' + line).join('\\n');
case 'ul':
// Handle unordered lists
const ulItems = Array.from(node.querySelectorAll('li'));
return ulItems.map(li => '- ' + li.textContent).join('\\n');
case 'ol':
// Handle ordered lists
const olItems = Array.from(node.querySelectorAll('li'));
return olItems.map((li, index) => (index + 1) + '. ' + li.textContent).join('\\n');
case 'br':
return '\\n';
default:
return node.textContent;
}
}
return '';
};
// Process each child node and add appropriate spacing
const nodes = Array.from(temp.childNodes).filter(node =>
node.nodeType === Node.ELEMENT_NODE ||
(node.nodeType === Node.TEXT_NODE && node.textContent.trim())
);
nodes.forEach((node, index) => {
const result = processNode(node);
if (result.trim()) {
if (index > 0 && markdown.trim()) {
markdown += '\\n\\n';
}
markdown += result;
}
});
return markdown.trim();
}
parseOriginalMarkdown() {
// Initialize the original markdown map - actual mapping happens in markSections
// to ensure alignment between HTML elements and markdown content
console.log('📝 Initializing original markdown mapping system');
// The actual mapping happens during markSections() to ensure HTML-markdown alignment
}
acceptSectionEdit(section, textarea) {
const sectionId = section.getAttribute('data-section');
console.log(`✅ Accepting edit for section ${sectionId}`);
// Clear any temporary edit for this section
if (this.tempEditMap && this.tempEditMap.has(sectionId)) {
this.tempEditMap.delete(sectionId);
console.log(`🗑️ Cleared temporary edit for section ${sectionId}`);
}
// Manually trigger the save logic
this.saveTextareaContent(textarea);
}
cancelSectionEdit(section, sectionId) {
console.log(`❌ Canceling edit for section ${sectionId}`);
// Clear any temporary edit for this section
if (this.tempEditMap && this.tempEditMap.has(sectionId)) {
this.tempEditMap.delete(sectionId);
console.log(`🗑️ Cleared temporary edit for section ${sectionId}`);
}
// Restore the original content without saving
const originalMarkdown = this.originalMarkdownMap.get(sectionId);
if (originalMarkdown) {
// Restore to original markdown content
const wrapper = document.createElement('div');
wrapper.innerHTML = marked.parse(originalMarkdown);
wrapper.classList.add('markitect-section-editable');
wrapper.setAttribute('data-section', sectionId);
// Remove data-edited and data-temp-edit to show it's back to original
wrapper.removeAttribute('data-edited');
wrapper.removeAttribute('data-temp-edit');
section.parentNode.replaceChild(wrapper, section);
} else {
console.warn(`⚠️ No original markdown found for section ${sectionId}`);
// Fallback: just remove the editing interface
if (section.parentNode) {
section.parentNode.removeChild(section);
}
}
// DON'T call markSections - it causes re-indexing issues
// this.markSections(document.getElementById('markdown-content'));
this.updateSaveStatus();
}
resetSectionEdit(section, sectionId, textarea) {
console.log(`🔄 Resetting edit for section ${sectionId}`);
// Reset textarea content to original markdown
const originalMarkdown = this.originalMarkdownMap.get(sectionId);
if (originalMarkdown) {
textarea.value = originalMarkdown;
console.log(`🔄 Reset textarea content to: "${originalMarkdown}"`);
} else {
console.warn(`⚠️ No original markdown found for section ${sectionId}`);
}
}
setupKeyboardShortcuts() {
if (MARKITECT_EDITOR_CONFIG.keyboardShortcuts) {
document.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.metaKey) {
switch(event.key) {
case 's':
event.preventDefault();
this.save();
break;
case 'e':
event.preventDefault();
this.togglePreview();
break;
case 'r':
event.preventDefault();
this.resetToOriginal();
break;
}
}
});
}
}
save() {
try {
// Get the current markdown content from the editor
const markdownContent = this.getMarkdownContent();
// Create filename with timestamp suffix for backup convention
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
const originalFilename = window.location.pathname.split('/').pop().replace('.html', '.md');
const backupFilename = `${originalFilename.replace('.md', '')}-edited-${timestamp}.md`;
// Create and download the file
const blob = new Blob([markdownContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = backupFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Update status with filename convention info
const statusEl = document.getElementById('save-status');
statusEl.textContent = `Downloaded: ${backupFilename}`;
statusEl.title = 'File saved with timestamp to avoid overwriting original';
setTimeout(() => {
this.updateSaveStatus();
}, 5000);
} catch (error) {
document.getElementById('save-status').textContent = 'Save failed!';
console.error('Save error:', error);
setTimeout(() => {
this.updateSaveStatus();
}, 3000);
}
}
updateSaveStatus() {
const editedSections = document.querySelectorAll('[data-edited]').length;
const totalSections = document.querySelectorAll('.markitect-section-editable').length;
const statusEl = document.getElementById('save-status');
if (editedSections === 0) {
statusEl.textContent = 'Ready to save';
statusEl.title = '';
} else {
statusEl.textContent = `Ready (${editedSections}/${totalSections} sections edited)`;
statusEl.title = 'Some sections have been modified from original';
}
}
getMarkdownContent() {
// If no edits have been made, return the original markdown content
if (!this.hasEdits) {
return markdownContent;
}
// Reconstruct markdown content from the current state of sections
const content = document.getElementById('markdown-content');
if (!content) {
return markdownContent; // fallback to original
}
// Simple approach: get the text content and convert back to markdown
// This is a basic implementation - could be enhanced for better preservation
const sections = content.querySelectorAll('.markitect-section-editable');
let reconstructed = '';
sections.forEach(section => {{
// Handle edited wrappers differently
if (section.hasAttribute('data-edited')) {{
// For edited sections, convert the child elements back to markdown
const childElements = section.children;
for (let i = 0; i < childElements.length; i++) {{
const child = childElements[i];
const tagName = child.tagName.toLowerCase();
const text = child.textContent.trim();
if (tagName.startsWith('h')) {{
const level = parseInt(tagName.charAt(1));
reconstructed += '#'.repeat(level) + ' ' + text + '\\n\\n';
}} else if (tagName === 'p') {{
reconstructed += text + '\\n\\n';
}} else if (tagName === 'blockquote') {{
reconstructed += '> ' + text + '\\n\\n';
}} else if (tagName === 'pre') {{
reconstructed += '```\\n' + text + '\\n```\\n\\n';
}} else if (tagName === 'ul') {{
const items = child.querySelectorAll('li');
items.forEach(item => {{
reconstructed += '- ' + item.textContent.trim() + '\\n';
}});
reconstructed += '\\n';
}} else if (tagName === 'ol') {{
const items = child.querySelectorAll('li');
items.forEach((item, index) => {{
reconstructed += (index + 1) + '. ' + item.textContent.trim() + '\\n';
}});
reconstructed += '\\n';
}} else {{
reconstructed += text + '\\n\\n';
}}
}}
}} else {{
// Handle regular sections
const tagName = section.tagName.toLowerCase();
const text = section.textContent.trim();
if (tagName.startsWith('h')) {{
const level = parseInt(tagName.charAt(1));
reconstructed += '#'.repeat(level) + ' ' + text + '\\n\\n';
}} else if (tagName === 'p') {{
reconstructed += text + '\\n\\n';
}} else if (tagName === 'blockquote') {{
reconstructed += '> ' + text + '\\n\\n';
}} else if (tagName === 'pre') {{
reconstructed += '```\\n' + text + '\\n```\\n\\n';
}} else if (tagName === 'ul') {{
const items = section.querySelectorAll('li');
items.forEach(item => {{
reconstructed += '- ' + item.textContent.trim() + '\\n';
}});
reconstructed += '\\n';
}} else if (tagName === 'ol') {{
const items = section.querySelectorAll('li');
items.forEach((item, index) => {{
reconstructed += (index + 1) + '. ' + item.textContent.trim() + '\\n';
}});
reconstructed += '\\n';
}} else {{
reconstructed += text + '\\n\\n';
}}
}}
}});
return reconstructed.trim();
}
togglePreview() {
console.log('Toggle preview mode');
}
resetToOriginal() {
if (confirm('Reset all content to original markdown? This will lose all edits.')) {
// Clear all edits and reload original content
const content = document.getElementById('markdown-content');
if (content && typeof marked !== 'undefined') {
content.innerHTML = marked.parse(markdownContent);
this.hasEdits = false;
this.markSections(content);
console.log('🔄 Reset to original content');
}
}
}
resetSectionToOriginal(sectionId) {
const originalMarkdown = this.originalMarkdownMap.get(sectionId);
if (originalMarkdown) {
// Find the section and reset it
const section = document.querySelector(`[data-section="${sectionId}"]`);
if (section && typeof marked !== 'undefined') {
const wrapper = document.createElement('div');
wrapper.innerHTML = marked.parse(originalMarkdown);
wrapper.classList.add('markitect-section-editable');
wrapper.setAttribute('data-section', sectionId);
// Remove data-edited to show it's back to original
wrapper.removeAttribute('data-edited');
section.parentNode.replaceChild(wrapper, section);
this.markSections(document.getElementById('markdown-content'));
this.updateSaveStatus();
console.log(`🔄 Reset section ${sectionId} to original`);
}
}
}
}
let markitectEditor;
// Control panel toggle functionality
function toggleControlPanel() {
const panel = document.getElementById('markitect-control-panel');
if (panel) {
panel.classList.toggle('expanded');
}
}
// Auto-close panel when clicking outside
document.addEventListener('click', function(event) {
const panel = document.getElementById('markitect-control-panel');
if (panel && panel.classList.contains('expanded')) {
if (!panel.contains(event.target)) {
panel.classList.remove('expanded');
}
}
// Legacy editor architecture loaded above
"""
# Clean mode doesn't need legacy control panel
edit_mode_html = ""
version = "0.3.0" # fallback
try:
from importlib.metadata import version as get_version
version = get_version('markitect')
except:
pass
# Get git commit with timestamp and local changes info
git_info = ""
try:
repo_path = Path(__file__).parent.parent
# Get commit hash and timestamp
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
capture_output=True, text=True, cwd=repo_path)
if result.returncode == 0:
commit_hash = result.stdout.strip()
# Get commit timestamp
timestamp_result = subprocess.run(['git', 'show', '-s', '--format=%ci', 'HEAD'],
capture_output=True, text=True, cwd=repo_path)
commit_time = ""
if timestamp_result.returncode == 0:
from datetime import datetime
# Parse git timestamp and format it nicely
git_time = timestamp_result.stdout.strip()
try:
dt = datetime.fromisoformat(git_time.replace(' +', '+'))
commit_time = f" ({dt.strftime('%Y-%m-%d %H:%M')})"
except:
pass
git_info = f"+{commit_hash}{commit_time}"
# Check for uncommitted changes
status_result = subprocess.run(['git', 'status', '--porcelain'],
capture_output=True, text=True, cwd=repo_path)
if status_result.returncode == 0 and status_result.stdout.strip():
# Get timestamp of most recent uncommitted change
import os
import glob
latest_change = 0
for line in status_result.stdout.strip().split('\n'):
if line.strip():
# Extract filename (skip first 3 chars which are status indicators)
filename = line[3:].strip()
try:
file_path = repo_path / filename
if file_path.exists():
mtime = os.path.getmtime(file_path)
latest_change = max(latest_change, mtime)
except:
pass
if latest_change > 0:
change_dt = datetime.fromtimestamp(latest_change)
git_info += f" including local changes until {change_dt.strftime('%Y-%m-%d %H:%M')}"
except:
pass
version_info = f"{version}{git_info}"
except:
version_info = "0.3.0"
edit_mode_html = f"""
<!-- Floating Control Panel -->
<div id="markitect-control-panel" class="markitect-control-panel">
<!-- Control Ribbon - Always Visible -->
<div class="markitect-control-ribbon" onclick="toggleControlPanel()" title="MarkiTect Editor Controls">
📝
</div>
<!-- Panel Header -->
<div class="markitect-panel-header">
<h3 class="markitect-panel-title">📝 MarkiTect Editor</h3>
<p class="markitect-panel-version">v{version_info}</p>
<button class="markitect-panel-close" onclick="toggleControlPanel()" title="Close panel">×</button>
</div>
<!-- Panel Body -->
<div class="markitect-panel-body">
<!-- Status Section -->
<div class="markitect-status-section">
<div id="status-indicator" class="markitect-status-indicator loading">
<span class="markitect-status-icon">⏳</span>
<div class="markitect-status-text" id="status-message">Loading edit capabilities...</div>
</div>
<!-- Error Details (hidden by default) -->
<div id="error-details" class="markitect-error-details">
<div class="markitect-error-title">❌ Edit Mode Failed</div>
<div class="markitect-error-text" id="error-text"></div>
<div class="markitect-error-help">
📋 Browser: <span id="browser-info"></span><br>
🔗 <a href="https://github.com/anthropics/markitect/issues/new" target="_blank" style="color: #1976d2;">Report Issue</a>
</div>
</div>
</div>
<!-- Controls Section -->
<div class="markitect-controls-section">
<div class="markitect-controls-grid">
<button class="markitect-control-btn" onclick="markitectEditor.save()" title="Download edited content">
<span class="icon">💾</span>
Save & Download
</button>
<button class="markitect-control-btn secondary" onclick="markitectEditor.togglePreview()" title="Toggle preview mode">
<span class="icon">👁️</span>
Preview
</button>
<button class="markitect-control-btn secondary" onclick="markitectEditor.resetToOriginal()" title="Reset all content to original (Ctrl+R)">
<span class="icon">🔄</span>
Reset All
</button>
</div>
<div class="markitect-save-info">
<div id="save-status">Ready to save</div>
<div style="margin-top: 4px; opacity: 0.8;">Saves as: filename-edited-YYYY-MM-DD-HH-MM-SS.md</div>
</div>
</div>
</div>
</div>"""
html_template = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
{css_content}
{default_css}
{editor_css}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
onload="window.markitectMarkedLoaded = true"
onerror="window.markitectMarkedError = true"></script>
</head>
<body{body_classes}>
{edit_mode_html}
<div id="markdown-content"></div>
<script>
const markdownContent = {js_markdown_content};
{editor_config}
// Define editor class first (if in edit mode)
{editor_scripts if edit_mode else ''}
{utility_functions}
// Always render content first (graceful degradation)
document.addEventListener('DOMContentLoaded', function() {{
{'console.log("Rendering content...");' if edit_type == 'clean' else 'updateStatus("Rendering content...");'}
const contentDiv = document.getElementById('markdown-content');
// Step 1: Ensure content is always displayed
if (contentDiv) {{
if (typeof marked !== 'undefined') {{
try {{
contentDiv.innerHTML = marked.parse(markdownContent);
{'console.log("✓ Content rendered successfully");' if edit_type == 'clean' else 'updateStatus("Content rendered successfully ✓");'}
console.log('✓ Markdown rendered successfully');
}} catch (error) {{
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
{'console.error("Content rendered with errors");' if edit_type == 'clean' else 'updateStatus("Content rendered with errors", true);'}
{'console.error("Markdown parsing failed:", error.message);' if edit_type == 'clean' else 'reportEditModeError("Markdown parsing failed", error.message);' if edit_mode else ''}
}}
}} else {{
// Fallback: display raw markdown with basic formatting
const fallbackHtml = markdownContent
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')
.replace(/\\*(.*?)\\*/g, '<em>$1</em>')
.replace(/^- (.*$)/gim, '<li>$1</li>')
.replace(/\\n\\n/g, '<br><br>')
.replace(/\\n/g, '<br>');
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
{'console.warn("Content rendered with fallback parser");' if edit_type == 'clean' else 'updateStatus("Content rendered with fallback parser", true);'}
{'console.warn("CDN library failed to load - using basic fallback rendering");' if edit_type == 'clean' else 'reportEditModeError("CDN library failed to load", "Using basic fallback rendering");' if edit_mode else ''}
}}
}}
// Step 2: Try to enhance with edit capabilities (if in edit mode)
{f'''if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {{
{'console.log("Initializing clean edit capabilities...");' if edit_type == 'clean' else 'updateStatus("Initializing legacy edit capabilities...");'}
try {{
{'console.log("Creating clean editor instance..."); initializeCleanEditor(); console.log("✓ Clean edit mode active - click any section to edit");' if edit_type == 'clean' else 'updateStatus("Creating legacy editor instance..."); markitectEditor = new MarkitectEditor(); markitectEditor.initializeEditor(); updateStatus("✓ Legacy edit mode active - click any section to edit"); console.log("✓ Legacy edit mode initialized successfully");'}
}} catch (error) {{
{'console.error("Clean edit mode failed to initialize:", error);' if edit_type == 'clean' else 'updateStatus("Legacy edit mode failed to initialize", true); console.error("Legacy editor error:", error);'}
}}
}}''' if edit_mode else ''}
}});
// Handle CDN loading errors
window.addEventListener('load', function() {{
if (window.markitectMarkedError) {{
{'console.error("CDN library failed to load - network or firewall blocking marked.js");' if edit_type == 'clean' else 'reportEditModeError("CDN library failed to load", "Network or firewall blocking marked.js");' if edit_mode else ''}
}}
}});
// Safety timeout for edit mode initialization
{'''setTimeout(function() {
const statusMsg = document.getElementById("status-message");
if (statusMsg && (statusMsg.textContent.includes("Loading") || statusMsg.textContent.includes("Initializing"))) {
updateStatus("Edit mode initialization timeout", true);
reportEditModeError("Edit mode took too long to initialize", "Possible JavaScript performance issue");
}
}, 5000); // 5 second timeout''' if edit_mode and edit_type == 'legacy' else ''}
</script>
</body>
</html>"""
return html_template
def _get_template_css(self, template: str = None) -> str:
"""Get CSS styles for the specified template theme."""
if template == 'github':
return """
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
color: #24292f;
background: #ffffff;
}
#markdown-content {
min-height: 200px;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
h1 { border-bottom: 1px solid #d0d7de; padding-bottom: .3em; }
h2 { border-bottom: 1px solid #d0d7de; padding-bottom: .3em; }
pre {
background: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #d0d7de;
}
code {
background: rgba(175,184,193,0.2);
padding: 0.2em 0.4em;
border-radius: 6px;
font-size: 0.85em;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #d0d7de;
margin: 0 0 16px 0;
padding: 0 1em;
color: #656d76;
}
"""
elif template == 'dark':
return """
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
color: #e1e4e8;
background-color: #0d1117;
}
#markdown-content {
min-height: 200px;
}
h1, h2, h3, h4, h5, h6 {
color: #58a6ff;
border-color: #30363d;
}
h1 { border-bottom: 1px solid #30363d; padding-bottom: .3em; }
h2 { border-bottom: 1px solid #30363d; padding-bottom: .3em; }
pre {
background-color: #161b22;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #30363d;
}
code {
background: #6e768166;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
color: #e1e4e8;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #58a6ff;
margin: 0;
padding-left: 1rem;
color: #8b949e;
}
a { color: #58a6ff; }
a:hover { color: #79c0ff; }
"""
elif template == 'academic':
return """
body {
font-family: Georgia, 'Times New Roman', serif;
max-width: 650px;
margin: 0 auto;
padding: 1rem;
line-height: 1.8;
color: #333;
background: #fff;
}
#markdown-content {
min-height: 200px;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
margin-top: 2rem;
margin-bottom: 1rem;
}
pre {
background: #f8f8f8;
padding: 1rem;
border-left: 4px solid #ccc;
overflow-x: auto;
font-family: 'Courier New', monospace;
}
code {
background: #f0f0f0;
padding: 0.1em 0.3em;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #ddd;
margin: 0;
padding-left: 1rem;
color: #666;
font-style: italic;
}
"""
else: # basic or default
return """
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
color: #333;
}
#markdown-content {
min-height: 200px;
}
pre {
background: #f6f8fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
}
code {
background: #f6f8fa;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #dfe2e5;
margin: 0;
padding-left: 1rem;
color: #6a737d;
}
"""