Files
markitect-main/markitect/document_manager.py
tegwick 3a53e0aa58
Some checks failed
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
fix: resolve md-render --edit functionality and add enhanced version tracking
This commit fixes the critical md-render --edit regression that was causing
"blue box, no content" issues and adds comprehensive version tracking.

Key fixes:
- Fixed JavaScript newline escaping in f-string templates (\\n\\n not \\\\n\\\\n)
- Restored proper content rendering with marked.js CDN and graceful fallback
- Removed problematic validation logic that was blocking content display
- Cleaned up html-inject-editing command and related experimental code

Enhancements:
- Added version display in edit mode header with git commit and timestamp
- Enhanced version tracking to show local uncommitted changes with timestamps
- Added comprehensive regression tests to prevent future breakage
- Improved error handling and recovery mechanisms

The md-render --edit functionality now works reliably with full version visibility.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 18:16:25 +02:00

1040 lines
40 KiB
Python

"""
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)
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
# Escape the markdown content for JavaScript
js_markdown_content = json.dumps(markdown_content)
# 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"'
editor_css = """
<style>
.markitect-floating-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid #ddd;
padding: 10px;
z-index: 1000;
backdrop-filter: blur(5px);
}
.markitect-section-editable {
border: 1px dashed transparent;
padding: 8px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
}
.markitect-section-editable:hover {
border-color: #007acc;
background: rgba(0, 122, 204, 0.05);
}
.edit-mode textarea {
width: 100%;
min-height: 100px;
font-family: monospace;
border: 2px solid #007acc;
border-radius: 4px;
padding: 8px;
}
</style>"""
editor_config = f"""
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: true,
sections: true
}};"""
editor_scripts = """
class MarkitectEditor {
constructor() {
this.initializeEditor();
this.setupKeyboardShortcuts();
}
initializeEditor() {
const header = document.createElement('div');
header.className = 'markitect-floating-header';
header.innerHTML = `
<button onclick="markitectEditor.save()" title="Download edited file with timestamp">💾 Save & Download</button>
<button onclick="markitectEditor.togglePreview()" title="Toggle preview mode">👁️ Preview</button>
<span id="save-status" style="margin-left: 15px; font-size: 0.9em;">Ready</span>
<span style="margin-left: 15px; font-size: 0.8em; color: #666;">
Saves as: filename-edited-YYYY-MM-DD-HH-MM-SS.md
</span>
`;
document.body.insertBefore(header, document.body.firstChild);
this.makeContentEditable();
}
makeContentEditable() {
const content = document.getElementById('markdown-content');
if (content) {
content.addEventListener('click', this.handleSectionClick.bind(this));
this.markSections(content);
}
}
markSections(element) {
const sections = element.querySelectorAll('h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol');
sections.forEach((section, index) => {
section.classList.add('markitect-section-editable');
section.setAttribute('data-section', index);
});
}
handleSectionClick(event) {
const section = event.target.closest('.markitect-section-editable');
if (section && !section.querySelector('textarea')) {
this.editSection(section);
}
}
editSection(section) {
const originalContent = section.innerHTML;
const textarea = document.createElement('textarea');
textarea.value = this.htmlToMarkdown(originalContent);
textarea.className = 'edit-mode';
textarea.addEventListener('blur', () => {
section.innerHTML = marked.parse(textarea.value);
this.markSections(section.parentElement);
});
section.innerHTML = '';
section.appendChild(textarea);
textarea.focus();
}
htmlToMarkdown(html) {
// Simple HTML to Markdown conversion
return html.replace(/<[^>]*>/g, '').trim();
}
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;
}
}
});
}
}
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(() => {
statusEl.textContent = 'Ready';
statusEl.title = '';
}, 5000);
} catch (error) {
document.getElementById('save-status').textContent = 'Save failed!';
console.error('Save error:', error);
setTimeout(() => {
document.getElementById('save-status').textContent = 'Ready';
}, 3000);
}
}
getMarkdownContent() {
// 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 => {{
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');
}
}
let markitectEditor;"""
# Edit mode status and error reporting section
edit_mode_html = ""
if edit_mode:
# Get version info for header
try:
import markitect
from pathlib import Path
import subprocess
# Get base version
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"""
<div id="markitect-status" style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 12px; margin-bottom: 20px; font-family: monospace; font-size: 14px;">
<div style="font-weight: bold; color: #1976d2;">📝 Markitect Edit Mode <span style="font-weight: normal; color: #666;">v{version_info}</span></div>
<div id="status-message" style="margin-top: 8px;">Loading edit capabilities...</div>
<div id="error-details" style="display: none; background: #ffebee; border: 1px solid #f44336; padding: 8px; margin-top: 8px; border-radius: 4px;">
<div style="font-weight: bold; color: #c62828;">❌ Edit Mode Failed</div>
<div id="error-text" style="margin-top: 4px; color: #666;"></div>
<details style="margin-top: 8px;">
<summary style="cursor: pointer; color: #1976d2;">🐛 Help us fix this issue</summary>
<div style="margin-top: 8px; font-size: 12px; color: #666;">
Please report this error with your browser info:
<br>📋 Browser: <span id="browser-info"></span>
<br>🔗 Create issue: <a href="https://github.com/anthropics/markitect/issues/new" target="_blank" style="color: #1976d2;">GitHub Issues</a>
</div>
</details>
</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 ''}
// Enhanced error reporting utility
function reportEditModeError(errorMsg, technicalDetails, errorType = 'error') {{
const statusDiv = document.getElementById('markitect-status');
const errorDiv = document.getElementById('error-details');
const errorText = document.getElementById('error-text');
const statusMsg = document.getElementById('status-message');
const browserInfo = document.getElementById('browser-info');
// Log to console for debugging
console.error('[MarkiTect Edit Mode Error]', errorMsg, technicalDetails);
// Create error report object
const errorReport = {{
timestamp: new Date().toISOString(),
error: errorMsg,
details: technicalDetails,
type: errorType,
userAgent: navigator.userAgent,
url: window.location.href,
markdownContent: typeof markdownContent !== 'undefined' ? markdownContent.length + ' chars' : 'unavailable'
}};
// Store error for potential reporting
if (!window.markitectErrors) window.markitectErrors = [];
window.markitectErrors.push(errorReport);
// Update UI
if (statusMsg) {{
const statusText = errorType === 'warning'
? 'Edit mode partially available - some features may not work'
: 'Edit mode unavailable - content displayed in read-only mode';
statusMsg.textContent = statusText;
}}
if (errorDiv) errorDiv.style.display = 'block';
if (errorText) {{
const fullError = errorMsg + (technicalDetails ? ' (' + technicalDetails + ')' : '');
errorText.textContent = fullError;
}}
if (browserInfo) browserInfo.textContent = navigator.userAgent.split(' ').slice(-2).join(' ');
// Auto-hide warnings after 10 seconds
if (errorType === 'warning' && errorDiv) {{
setTimeout(() => {{
errorDiv.style.display = 'none';
}}, 10000);
}}
}}
// Status update utility
function updateStatus(message, isError = false) {{
const statusMsg = document.getElementById('status-message');
if (statusMsg) {{
statusMsg.textContent = message;
statusMsg.style.color = isError ? '#c62828' : '#1976d2';
}}
}}
// Always render content first (graceful degradation)
document.addEventListener('DOMContentLoaded', function() {{
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);
updateStatus('Content rendered successfully ✓');
console.log('✓ Markdown rendered successfully');
}} catch (error) {{
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
updateStatus('Content rendered with errors', true);
{'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>';
updateStatus('Content rendered with fallback parser', true);
{'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)
{'''if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {
updateStatus("Initializing edit capabilities...");
try {
updateStatus("Creating editor instance...");
markitectEditor = new MarkitectEditor();
updateStatus("✓ Edit mode active - click any section to edit");
console.log("✓ Edit mode initialized successfully");
} catch (error) {
updateStatus("Edit mode failed to initialize", true);
reportEditModeError("Edit mode initialization failed", error.message);
console.error("Edit mode error:", error);
}
}''' if edit_mode else ''}
}});
// Handle CDN loading errors
window.addEventListener('load', function() {{
if (window.markitectMarkedError) {{
{'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);''' if edit_mode else ''} // 5 second timeout
</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;
}
"""