fix: resolve md-render --edit functionality and add enhanced version tracking
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

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>
This commit is contained in:
2025-10-25 18:16:25 +02:00
parent 64d1606740
commit 3a53e0aa58
3 changed files with 151 additions and 687 deletions

View File

@@ -1501,8 +1501,7 @@ class MarkdownCommandsPlugin(CommandPlugin):
'md-explode': md_explode_command,
'md-implode': md_implode_command,
'md-package': md_package_command,
'md-transclude': md_transclude_command,
'html-inject-editing': html_inject_editing
'md-transclude': md_transclude_command
}
@@ -2984,542 +2983,3 @@ class FilenameDecoder:
return [self.decode(filename) for filename in filenames]
# ==============================================================================
# HTML Editing Injection Command - Graceful Enhancement System
# ==============================================================================
@click.command()
@click.argument('html_file', type=click.Path(exists=True))
@click.option('--output', '-o', type=click.Path(),
help='Output HTML file with editing capabilities (default: <input>-editable.html)')
@click.option('--editor-theme', default='github',
type=click.Choice(['github', 'monokai', 'tomorrow', 'dark']),
help='Editor theme for edit mode (default: github)')
@click.option('--keyboard-shortcuts', is_flag=True, default=True,
help='Enable keyboard shortcuts in edit mode')
@click.option('--fallback-mode', type=click.Choice(['graceful', 'minimal', 'none']),
default='graceful', help='Fallback strategy when JavaScript fails')
@click.option('--backup/--no-backup', default=True,
help='Create backup of original file')
@click.option('--dry-run', is_flag=True,
help='Show what would be done without making changes')
@click.pass_context
def html_inject_editing(ctx, html_file, output, editor_theme, keyboard_shortcuts,
fallback_mode, backup, dry_run):
"""
Inject editing capabilities into existing HTML files.
This command adds JavaScript editing functionality to any HTML file
containing markdown content. It provides graceful fallback when
JavaScript fails, ensuring the document remains readable.
HTML_FILE: Path to the HTML file to enhance with editing capabilities
Fallback modes:
graceful - Full fallback with basic editing via contenteditable
minimal - Fallback to read-only with error messages
none - No fallback, editing simply won't work if JS fails
Examples:
markitect html-inject-editing document.html
markitect html-inject-editing doc.html --output doc-editable.html
markitect html-inject-editing page.html --fallback-mode minimal --no-backup
"""
config = ctx.obj or {}
try:
input_path = Path(html_file)
# Determine output path
if output:
output_path = Path(output)
else:
# Create name like "document-editable.html"
stem = input_path.stem
suffix = input_path.suffix
output_path = input_path.parent / f"{stem}-editable{suffix}"
if dry_run:
click.echo(f"🔍 Would inject editing capabilities into: {input_path}")
click.echo(f"📝 Would create enhanced file: {output_path}")
click.echo(f"🎨 Editor theme: {editor_theme}")
click.echo(f"⌨️ Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
click.echo(f"🛡️ Fallback mode: {fallback_mode}")
if backup:
backup_path = input_path.parent / f"{input_path.stem}.backup{input_path.suffix}"
click.echo(f"💾 Would create backup: {backup_path}")
return
# Create backup if requested
if backup and not output:
backup_path = input_path.parent / f"{input_path.stem}.backup{input_path.suffix}"
backup_path.write_text(input_path.read_text(encoding='utf-8'), encoding='utf-8')
click.echo(f"💾 Created backup: {backup_path}")
# Read original HTML
html_content = input_path.read_text(encoding='utf-8')
# Inject editing capabilities
enhanced_html = inject_editing_capabilities(
html_content=html_content,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
fallback_mode=fallback_mode
)
# Write enhanced HTML
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(enhanced_html, encoding='utf-8')
click.echo(f"✨ Enhanced HTML with editing capabilities: {output_path}")
click.echo(f"🎨 Editor theme: {editor_theme}")
click.echo(f"🛡️ Fallback mode: {fallback_mode}")
if config.get('verbose', False):
click.echo(f"⌨️ Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
click.echo(f"📄 Original size: {len(html_content)} chars")
click.echo(f"📄 Enhanced size: {len(enhanced_html)} chars")
except Exception as e:
click.echo(f"Error injecting editing capabilities: {e}", err=True)
raise click.Abort()
def inject_editing_capabilities(html_content: str, editor_theme: str = 'github',
keyboard_shortcuts: bool = True,
fallback_mode: str = 'graceful') -> str:
"""
Inject editing capabilities into HTML content with graceful fallback.
This function adds editing functionality that degrades gracefully:
1. Full editing if JavaScript loads successfully
2. Basic contenteditable if CDN fails but DOM works
3. Read-only mode with clear error messages if everything fails
"""
import re
# Generate the editing enhancement script
enhancement_script = generate_editing_enhancement_script(
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
fallback_mode=fallback_mode
)
# Try to inject before closing </body> tag
body_close_pattern = r'</body>'
if re.search(body_close_pattern, html_content, re.IGNORECASE):
enhanced = re.sub(
body_close_pattern,
f'{enhancement_script}\n</body>',
html_content,
flags=re.IGNORECASE
)
return enhanced
# Fallback: inject before closing </html> tag
html_close_pattern = r'</html>'
if re.search(html_close_pattern, html_content, re.IGNORECASE):
enhanced = re.sub(
html_close_pattern,
f'{enhancement_script}\n</html>',
html_content,
flags=re.IGNORECASE
)
return enhanced
# Last fallback: append to end of content
return html_content + '\n' + enhancement_script
def generate_editing_enhancement_script(editor_theme: str = 'github',
keyboard_shortcuts: bool = True,
fallback_mode: str = 'graceful') -> str:
"""
Generate the JavaScript enhancement script with graceful fallback.
This creates a self-contained script that:
1. Attempts to load required libraries from CDN
2. Falls back gracefully if CDN fails
3. Provides basic editing even without external dependencies
"""
fallback_css = """
<style id="markitect-fallback-styles">
.markitect-edit-fallback {
border: 2px dashed #ffa500;
background-color: #fff3cd;
padding: 10px;
margin: 5px 0;
border-radius: 4px;
cursor: text;
}
.markitect-edit-fallback:hover {
background-color: #ffeaa7;
border-color: #e17055;
}
.markitect-edit-fallback[contenteditable="true"] {
border-color: #00b894;
background-color: #d1f2eb;
outline: none;
}
.markitect-fallback-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #fd7f00;
color: white;
padding: 8px 15px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.markitect-fallback-warning {
background: #e74c3c;
color: white;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body { margin-top: 50px !important; }
</style>
"""
script_content = f"""
{fallback_css}
<script>
(function() {{
'use strict';
// Configuration
const CONFIG = {{
editorTheme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
fallbackMode: '{fallback_mode}'
}};
// State management
let editingState = {{
isEditMode: false,
currentEditElement: null,
originalContent: new Map(),
hasErrors: false,
cdnLoaded: false
}};
// Graceful enhancement entry point
function initializeEditingEnhancement() {{
console.log('[MarkiTect] Initializing editing enhancement...');
// Step 1: Try to load external dependencies
loadExternalDependencies()
.then(() => {{
console.log('[MarkiTect] External libraries loaded successfully');
editingState.cdnLoaded = true;
initializeFullEditor();
}})
.catch((error) => {{
console.warn('[MarkiTect] CDN loading failed:', error);
editingState.hasErrors = true;
if (CONFIG.fallbackMode === 'graceful') {{
initializeFallbackEditor();
}} else if (CONFIG.fallbackMode === 'minimal') {{
showMinimalFallback();
}} else {{
showNoFallback();
}}
}});
}}
// Load external dependencies (marked.js, etc.)
function loadExternalDependencies() {{
return new Promise((resolve, reject) => {{
// Check if marked is already available
if (typeof marked !== 'undefined') {{
resolve();
return;
}}
// Try to load marked.js from CDN
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
// Timeout after 5 seconds
const timeout = setTimeout(() => {{
reject(new Error('CDN loading timeout'));
}}, 5000);
script.onload = () => {{
clearTimeout(timeout);
if (typeof marked !== 'undefined') {{
resolve();
}} else {{
reject(new Error('marked.js loaded but not available'));
}}
}};
script.onerror = () => {{
clearTimeout(timeout);
reject(new Error('Failed to load marked.js'));
}};
document.head.appendChild(script);
}});
}}
// Full editor with all features
function initializeFullEditor() {{
console.log('[MarkiTect] Initializing full editor...');
addFloatingHeader('✨ Full Edit Mode Active - Click any section to edit');
makeContentEditable();
if (CONFIG.keyboardShortcuts) {{
setupKeyboardShortcuts();
}}
}}
// Fallback editor with basic functionality
function initializeFallbackEditor() {{
console.log('[MarkiTect] Initializing fallback editor...');
addFloatingHeader('⚠️ Fallback Edit Mode - Limited functionality (CDN failed)', 'warning');
makeContentEditableBasic();
}}
// Minimal mode with just error reporting
function showMinimalFallback() {{
console.log('[MarkiTect] Showing minimal fallback...');
addFloatingHeader('❌ Edit Mode Unavailable - Network issues detected', 'error');
addErrorMessage('Editing capabilities could not be loaded due to network restrictions. Document is read-only.');
}}
// No fallback mode
function showNoFallback() {{
console.log('[MarkiTect] No fallback mode - editing disabled');
// Silent failure - no editing capabilities
}}
// Add floating header for status
function addFloatingHeader(message, type = 'info') {{
const header = document.createElement('div');
header.className = 'markitect-fallback-header';
header.innerHTML = `
<span>${{message}}</span>
<button onclick="this.parentElement.style.display='none'" style="float: right; background: none; border: none; color: white; cursor: pointer;">×</button>
`;
document.body.insertBefore(header, document.body.firstChild);
}}
// Add error message to content
function addErrorMessage(message) {{
const errorDiv = document.createElement('div');
errorDiv.className = 'markitect-fallback-warning';
errorDiv.innerHTML = `
<strong>Editing Unavailable:</strong> ${{message}}
<br><small>This document remains fully readable. Editing requires JavaScript and network access.</small>
`;
const firstElement = document.body.firstElementChild;
if (firstElement) {{
document.body.insertBefore(errorDiv, firstElement.nextSibling);
}} else {{
document.body.appendChild(errorDiv);
}}
}}
// Make content editable with full markdown support
function makeContentEditable() {{
const sections = document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol, li');
sections.forEach((section, index) => {{
section.classList.add('markitect-section-editable');
section.setAttribute('data-section-id', index);
section.addEventListener('click', handleSectionClick);
section.style.cursor = 'pointer';
section.title = 'Click to edit this section';
}});
}}
// Basic contenteditable fallback
function makeContentEditableBasic() {{
const sections = document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, blockquote');
sections.forEach((section, index) => {{
section.classList.add('markitect-edit-fallback');
section.setAttribute('contenteditable', 'true');
section.setAttribute('data-section-id', index);
section.title = 'Basic editing mode - formatting may be limited';
// Store original content
editingState.originalContent.set(index, section.innerHTML);
// Add simple save/restore on blur
section.addEventListener('blur', () => {{
// Basic validation - could be enhanced
if (section.innerText.trim() === '') {{
section.innerHTML = editingState.originalContent.get(index);
}}
}});
}});
}}
// Handle section click for full editor
function handleSectionClick(event) {{
const section = event.target.closest('.markitect-section-editable');
if (!section) return;
const sectionId = section.getAttribute('data-section-id');
if (editingState.currentEditElement && editingState.currentEditElement !== section) {{
// Cancel current edit if clicking on different section
const currentId = editingState.currentEditElement.getAttribute('data-section-id');
cancelEdit(currentId);
}}
if (!section.querySelector('textarea')) {{
startEditing(section, sectionId);
}}
}}
// Start editing a section
function startEditing(section, sectionId) {{
editingState.originalContent.set(sectionId, section.innerHTML);
editingState.currentEditElement = section;
const textarea = document.createElement('textarea');
textarea.value = htmlToMarkdown(section.innerHTML);
textarea.style.cssText = `
width: 100%;
min-height: 100px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
border: 2px solid #007acc;
border-radius: 4px;
padding: 8px;
background: #f8f9fa;
resize: vertical;
`;
const controls = document.createElement('div');
controls.style.cssText = 'margin-top: 5px;';
controls.innerHTML = `
<button onclick="markitectApplyEdit('${{sectionId}}')" style="margin-right: 5px;">Apply</button>
<button onclick="markitectCancelEdit('${{sectionId}}')" style="margin-right: 5px;">Cancel</button>
<small style="color: #666;">Ctrl+Enter to apply, Esc to cancel</small>
`;
section.innerHTML = '';
section.appendChild(textarea);
section.appendChild(controls);
textarea.focus();
// Keyboard shortcuts for editing
textarea.addEventListener('keydown', (e) => {{
if (e.ctrlKey && e.key === 'Enter') {{
e.preventDefault();
applyEdit(sectionId);
}} else if (e.key === 'Escape') {{
e.preventDefault();
cancelEdit(sectionId);
}}
}});
}}
// Apply edit
function applyEdit(sectionId) {{
const section = document.querySelector(`[data-section-id="${{sectionId}}"]`);
const textarea = section.querySelector('textarea');
if (textarea && editingState.cdnLoaded && typeof marked !== 'undefined') {{
// Full markdown rendering
section.innerHTML = marked.parse(textarea.value);
}} else if (textarea) {{
// Basic text with line breaks
section.innerHTML = textarea.value.replace(/\\n/g, '<br>');
}}
editingState.currentEditElement = null;
makeContentEditable(); // Re-attach click handlers
}}
// Cancel edit
function cancelEdit(sectionId) {{
const section = document.querySelector(`[data-section-id="${{sectionId}}"]`);
section.innerHTML = editingState.originalContent.get(sectionId);
editingState.currentEditElement = null;
makeContentEditable(); // Re-attach click handlers
}}
// Global functions for button clicks
window.markitectApplyEdit = applyEdit;
window.markitectCancelEdit = cancelEdit;
// Simple HTML to Markdown conversion
function htmlToMarkdown(html) {{
return html
.replace(/<h([1-6])[^>]*>(.*?)<\\/h[1-6]>/gi, (match, level, text) => {{
return '#'.repeat(parseInt(level)) + ' ' + text.replace(/<[^>]*>/g, '') + '\\\\n\\\\n';
}})
.replace(/<p[^>]*>(.*?)<\\/p>/gi, '$1\\\\n\\\\n')
.replace(/<strong[^>]*>(.*?)<\\/strong>/gi, '**$1**')
.replace(/<em[^>]*>(.*?)<\\/em>/gi, '*$1*')
.replace(/<code[^>]*>(.*?)<\\/code>/gi, '`$1`')
.replace(/<br[^>]*>/gi, '\\\\n')
.replace(/<[^>]*>/g, '')
.trim();
}}
// Setup keyboard shortcuts
function setupKeyboardShortcuts() {{
document.addEventListener('keydown', (e) => {{
if ((e.ctrlKey || e.metaKey) && e.key === 's') {{
e.preventDefault();
saveDocument();
}}
}});
}}
// Save document functionality
function saveDocument() {{
const content = extractMarkdownContent();
const blob = new Blob([content], {{ type: 'text/markdown' }});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'edited-document.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}}
// Extract markdown content from current document
function extractMarkdownContent() {{
const sections = document.querySelectorAll('[data-section-id]');
let content = '';
sections.forEach(section => {{
content += htmlToMarkdown(section.innerHTML) + '\\\\n\\\\n';
}});
return content.trim();
}}
// Initialize when DOM is ready
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', initializeEditingEnhancement);
}} else {{
initializeEditingEnhancement();
}}
}})();
</script>
"""
return script_content