markitect-main/markitect/document_manager.py

2076 lines
84 KiB
Python
Raw Normal View History

"""
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
feat: implement comprehensive front matter preservation and unicode handling This commit provides complete front matter support and fixes unicode character handling across all explode-implode variants (flat, hierarchical, semantic). ## Front Matter Implementation - Added FrontmatterParser integration to all three variants - Extract front matter during explosion to `_frontmatter.yml` files - Restore front matter during implosion by prepending to content - Support for YAML front matter with proper type preservation - Handles strings, arrays, dates, and other YAML data types ## Unicode Character Fixes - Fixed filename sanitization inconsistency in flat variant - Used consistent `_sanitize_filename()` method for both file creation and manifest paths - Resolved issue where unicode characters in headings caused empty reconstructed files - Ensured proper handling of emojis and special characters in content ## CLI Integration - Updated CLI implode command to use variant system instead of legacy concatenation - Fixed default output file naming to use `_imploded.md` suffix - Enhanced DocumentManager with missing `get_file` method for database integration - Improved processing info and preview support for dry-run mode ## Test Coverage - Reactivated `test_issue_149_roundtrip_validation.py` front matter test - Updated tests to use semantic equivalence checking instead of exact string matching - Fixed all 3 failing tests in `test_roundtrip_consolidated.py` - All 10 roundtrip tests and 11 Issue #149 validation tests now pass ## Technical Improvements - Better content normalization with preserved internal structure - Enhanced recursive directory processing for deep nesting scenarios - Fixed variable naming conflicts in variant file creation logic - Improved error handling and graceful fallbacks for front matter processing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 20:26:08 +02:00
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);
}
feat: enhance empty line preservation and automatic paragraph separation Implemented sophisticated paragraph handling for the markdown editor: Enhanced HTML-to-Markdown Conversion: - Replaced simple tag stripping with proper structural parsing - Preserves formatting for headers, emphasis, code, blockquotes - Maintains paragraph separation with proper spacing - Handles nested elements and mixed content correctly Dynamic Section Splitting: - Detects paragraph breaks (double newlines) when editing - Automatically creates separate editable sections for each paragraph - Enables independent editing of logically separate content - Maintains proper section indexing with sub-identifiers Visual Enhancements: - Added green styling for edited sections to distinguish from originals - Subtle borders and backgrounds indicate modified content - Hover effects provide clear feedback on editable areas Technical Improvements: - Enhanced blur handler to detect multiple paragraphs - Smart wrapper creation for single vs. multi-paragraph content - Proper DOM manipulation for section insertion and replacement - Preserves editing state and section relationships Benefits: - Empty lines between paragraphs are preserved accurately - Text separated by empty lines becomes independently editable - Better content organization and editing granularity - Improved user experience with clear visual feedback This resolves the empty line swallowing issue and provides intuitive paragraph-level editing that matches user expectations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:35:53 +02:00
.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 */
feat: implement intelligent auto-sizing textarea for optimal editing UX Enhanced the editing experience with smart textarea sizing that adapts to content dimensions: Smart Auto-Sizing Logic: - Dynamically calculates height based on content lines - Minimum height: 3 lines (63px) for comfortable editing - Maximum height: 20 lines (444px) to prevent excessive expansion - Precise calculation using line-height and padding measurements Responsive Behavior: - Auto-resizes on input events as you type - Handles paste operations with proper sizing - Smooth transitions with 0.15s ease animation - Temporarily disables transition during measurement for accuracy Technical Implementation: - Line-height aware calculation (14px font × 1.5 = 21px per line) - Proper padding compensation (24px total) - Scroll-height based measurement for precise content fitting - Debounced initial sizing to handle DOM rendering User Experience Benefits: - Textarea perfectly fits content size on open - No unnecessary white space for short content - Sufficient space for longer content without overwhelming - Natural, document-like editing experience - Visual harmony with surrounding content boxes CSS Enhancements: - Reduced min-height from 100px to 60px for better proportions - Added smooth height transitions for polished feel - Maintained vertical resize capability for user control - Proper box-sizing for consistent measurements This creates a much more natural editing experience where the textarea intelligently adapts to match the content being edited. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:53:49 +02:00
min-height: 60px;
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
max-height: 360px;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
border: 2px solid #007acc;
border-radius: 6px;
padding: 12px;
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
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 */
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
overflow: auto;
feat: implement intelligent auto-sizing textarea for optimal editing UX Enhanced the editing experience with smart textarea sizing that adapts to content dimensions: Smart Auto-Sizing Logic: - Dynamically calculates height based on content lines - Minimum height: 3 lines (63px) for comfortable editing - Maximum height: 20 lines (444px) to prevent excessive expansion - Precise calculation using line-height and padding measurements Responsive Behavior: - Auto-resizes on input events as you type - Handles paste operations with proper sizing - Smooth transitions with 0.15s ease animation - Temporarily disables transition during measurement for accuracy Technical Implementation: - Line-height aware calculation (14px font × 1.5 = 21px per line) - Proper padding compensation (24px total) - Scroll-height based measurement for precise content fitting - Debounced initial sizing to handle DOM rendering User Experience Benefits: - Textarea perfectly fits content size on open - No unnecessary white space for short content - Sufficient space for longer content without overwhelming - Natural, document-like editing experience - Visual harmony with surrounding content boxes CSS Enhancements: - Reduced min-height from 100px to 60px for better proportions - Added smooth height transitions for polished feel - Maintained vertical resize capability for user control - Proper box-sizing for consistent measurements This creates a much more natural editing experience where the textarea intelligently adapts to match the content being edited. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:53:49 +02:00
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!`);
}
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
// 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;
}
feat: implement intelligent auto-sizing textarea for optimal editing UX Enhanced the editing experience with smart textarea sizing that adapts to content dimensions: Smart Auto-Sizing Logic: - Dynamically calculates height based on content lines - Minimum height: 3 lines (63px) for comfortable editing - Maximum height: 20 lines (444px) to prevent excessive expansion - Precise calculation using line-height and padding measurements Responsive Behavior: - Auto-resizes on input events as you type - Handles paste operations with proper sizing - Smooth transitions with 0.15s ease animation - Temporarily disables transition during measurement for accuracy Technical Implementation: - Line-height aware calculation (14px font × 1.5 = 21px per line) - Proper padding compensation (24px total) - Scroll-height based measurement for precise content fitting - Debounced initial sizing to handle DOM rendering User Experience Benefits: - Textarea perfectly fits content size on open - No unnecessary white space for short content - Sufficient space for longer content without overwhelming - Natural, document-like editing experience - Visual harmony with surrounding content boxes CSS Enhancements: - Reduced min-height from 100px to 60px for better proportions - Added smooth height transitions for polished feel - Maintained vertical resize capability for user control - Proper box-sizing for consistent measurements This creates a much more natural editing experience where the textarea intelligently adapts to match the content being edited. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:53:49 +02:00
// 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';
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
// Calculate based on actual content with more reasonable constraints
const contentHeight = textarea.scrollHeight;
feat: implement intelligent auto-sizing textarea for optimal editing UX Enhanced the editing experience with smart textarea sizing that adapts to content dimensions: Smart Auto-Sizing Logic: - Dynamically calculates height based on content lines - Minimum height: 3 lines (63px) for comfortable editing - Maximum height: 20 lines (444px) to prevent excessive expansion - Precise calculation using line-height and padding measurements Responsive Behavior: - Auto-resizes on input events as you type - Handles paste operations with proper sizing - Smooth transitions with 0.15s ease animation - Temporarily disables transition during measurement for accuracy Technical Implementation: - Line-height aware calculation (14px font × 1.5 = 21px per line) - Proper padding compensation (24px total) - Scroll-height based measurement for precise content fitting - Debounced initial sizing to handle DOM rendering User Experience Benefits: - Textarea perfectly fits content size on open - No unnecessary white space for short content - Sufficient space for longer content without overwhelming - Natural, document-like editing experience - Visual harmony with surrounding content boxes CSS Enhancements: - Reduced min-height from 100px to 60px for better proportions - Added smooth height transitions for polished feel - Maintained vertical resize capability for user control - Proper box-sizing for consistent measurements This creates a much more natural editing experience where the textarea intelligently adapts to match the content being edited. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:53:49 +02:00
const padding = 24; // 12px top + 12px bottom
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
// 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
feat: implement intelligent auto-sizing textarea for optimal editing UX Enhanced the editing experience with smart textarea sizing that adapts to content dimensions: Smart Auto-Sizing Logic: - Dynamically calculates height based on content lines - Minimum height: 3 lines (63px) for comfortable editing - Maximum height: 20 lines (444px) to prevent excessive expansion - Precise calculation using line-height and padding measurements Responsive Behavior: - Auto-resizes on input events as you type - Handles paste operations with proper sizing - Smooth transitions with 0.15s ease animation - Temporarily disables transition during measurement for accuracy Technical Implementation: - Line-height aware calculation (14px font × 1.5 = 21px per line) - Proper padding compensation (24px total) - Scroll-height based measurement for precise content fitting - Debounced initial sizing to handle DOM rendering User Experience Benefits: - Textarea perfectly fits content size on open - No unnecessary white space for short content - Sufficient space for longer content without overwhelming - Natural, document-like editing experience - Visual harmony with surrounding content boxes CSS Enhancements: - Reduced min-height from 100px to 60px for better proportions - Added smooth height transitions for polished feel - Maintained vertical resize capability for user control - Proper box-sizing for consistent measurements This creates a much more natural editing experience where the textarea intelligently adapts to match the content being edited. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:53:49 +02:00
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
const newHeight = Math.max(60, Math.min(maxHeight, Math.max(minHeight, contentHeight + 4)));
feat: implement intelligent auto-sizing textarea for optimal editing UX Enhanced the editing experience with smart textarea sizing that adapts to content dimensions: Smart Auto-Sizing Logic: - Dynamically calculates height based on content lines - Minimum height: 3 lines (63px) for comfortable editing - Maximum height: 20 lines (444px) to prevent excessive expansion - Precise calculation using line-height and padding measurements Responsive Behavior: - Auto-resizes on input events as you type - Handles paste operations with proper sizing - Smooth transitions with 0.15s ease animation - Temporarily disables transition during measurement for accuracy Technical Implementation: - Line-height aware calculation (14px font × 1.5 = 21px per line) - Proper padding compensation (24px total) - Scroll-height based measurement for precise content fitting - Debounced initial sizing to handle DOM rendering User Experience Benefits: - Textarea perfectly fits content size on open - No unnecessary white space for short content - Sufficient space for longer content without overwhelming - Natural, document-like editing experience - Visual harmony with surrounding content boxes CSS Enhancements: - Reduced min-height from 100px to 60px for better proportions - Added smooth height transitions for polished feel - Maintained vertical resize capability for user control - Proper box-sizing for consistent measurements This creates a much more natural editing experience where the textarea intelligently adapts to match the content being edited. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:53:49 +02:00
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
feat: enhance empty line preservation and automatic paragraph separation Implemented sophisticated paragraph handling for the markdown editor: Enhanced HTML-to-Markdown Conversion: - Replaced simple tag stripping with proper structural parsing - Preserves formatting for headers, emphasis, code, blockquotes - Maintains paragraph separation with proper spacing - Handles nested elements and mixed content correctly Dynamic Section Splitting: - Detects paragraph breaks (double newlines) when editing - Automatically creates separate editable sections for each paragraph - Enables independent editing of logically separate content - Maintains proper section indexing with sub-identifiers Visual Enhancements: - Added green styling for edited sections to distinguish from originals - Subtle borders and backgrounds indicate modified content - Hover effects provide clear feedback on editable areas Technical Improvements: - Enhanced blur handler to detect multiple paragraphs - Smart wrapper creation for single vs. multi-paragraph content - Proper DOM manipulation for section insertion and replacement - Preserves editing state and section relationships Benefits: - Empty lines between paragraphs are preserved accurately - Text separated by empty lines becomes independently editable - Better content organization and editing granularity - Improved user experience with clear visual feedback This resolves the empty line swallowing issue and provides intuitive paragraph-level editing that matches user expectations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:35:53 +02:00
// 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) {
feat: enhance empty line preservation and automatic paragraph separation Implemented sophisticated paragraph handling for the markdown editor: Enhanced HTML-to-Markdown Conversion: - Replaced simple tag stripping with proper structural parsing - Preserves formatting for headers, emphasis, code, blockquotes - Maintains paragraph separation with proper spacing - Handles nested elements and mixed content correctly Dynamic Section Splitting: - Detects paragraph breaks (double newlines) when editing - Automatically creates separate editable sections for each paragraph - Enables independent editing of logically separate content - Maintains proper section indexing with sub-identifiers Visual Enhancements: - Added green styling for edited sections to distinguish from originals - Subtle borders and backgrounds indicate modified content - Hover effects provide clear feedback on editable areas Technical Improvements: - Enhanced blur handler to detect multiple paragraphs - Smart wrapper creation for single vs. multi-paragraph content - Proper DOM manipulation for section insertion and replacement - Preserves editing state and section relationships Benefits: - Empty lines between paragraphs are preserved accurately - Text separated by empty lines becomes independently editable - Better content organization and editing granularity - Improved user experience with clear visual feedback This resolves the empty line swallowing issue and provides intuitive paragraph-level editing that matches user expectations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:35:53 +02:00
// 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();
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
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;
feat: enhance empty line preservation and automatic paragraph separation Implemented sophisticated paragraph handling for the markdown editor: Enhanced HTML-to-Markdown Conversion: - Replaced simple tag stripping with proper structural parsing - Preserves formatting for headers, emphasis, code, blockquotes - Maintains paragraph separation with proper spacing - Handles nested elements and mixed content correctly Dynamic Section Splitting: - Detects paragraph breaks (double newlines) when editing - Automatically creates separate editable sections for each paragraph - Enables independent editing of logically separate content - Maintains proper section indexing with sub-identifiers Visual Enhancements: - Added green styling for edited sections to distinguish from originals - Subtle borders and backgrounds indicate modified content - Hover effects provide clear feedback on editable areas Technical Improvements: - Enhanced blur handler to detect multiple paragraphs - Smart wrapper creation for single vs. multi-paragraph content - Proper DOM manipulation for section insertion and replacement - Preserves editing state and section relationships Benefits: - Empty lines between paragraphs are preserved accurately - Text separated by empty lines becomes independently editable - Better content organization and editing granularity - Improved user experience with clear visual feedback This resolves the empty line swallowing issue and provides intuitive paragraph-level editing that matches user expectations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:35:53 +02:00
}
}
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())
);
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
nodes.forEach((node, index) => {
feat: enhance empty line preservation and automatic paragraph separation Implemented sophisticated paragraph handling for the markdown editor: Enhanced HTML-to-Markdown Conversion: - Replaced simple tag stripping with proper structural parsing - Preserves formatting for headers, emphasis, code, blockquotes - Maintains paragraph separation with proper spacing - Handles nested elements and mixed content correctly Dynamic Section Splitting: - Detects paragraph breaks (double newlines) when editing - Automatically creates separate editable sections for each paragraph - Enables independent editing of logically separate content - Maintains proper section indexing with sub-identifiers Visual Enhancements: - Added green styling for edited sections to distinguish from originals - Subtle borders and backgrounds indicate modified content - Hover effects provide clear feedback on editable areas Technical Improvements: - Enhanced blur handler to detect multiple paragraphs - Smart wrapper creation for single vs. multi-paragraph content - Proper DOM manipulation for section insertion and replacement - Preserves editing state and section relationships Benefits: - Empty lines between paragraphs are preserved accurately - Text separated by empty lines becomes independently editable - Better content organization and editing granularity - Improved user experience with clear visual feedback This resolves the empty line swallowing issue and provides intuitive paragraph-level editing that matches user expectations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:35:53 +02:00
const result = processNode(node);
if (result.trim()) {
fix: resolve textarea sizing, font preservation, and markdown structure issues Addressed multiple critical editing experience issues: Enhanced Markdown Preservation: - Fixed htmlToMarkdown() to properly preserve heading hash signs (# ## ###) - Maintained markdown structure for lists, code blocks, and blockquotes - Preserved inline formatting (bold, italic, code) within paragraphs - Improved spacing and indentation handling for complex structures Font Size & Style Preservation: - Extract and apply original element's font-size to textarea - Preserve line-height from source content for consistent appearance - Use inherit values in CSS, overridden by JavaScript for accuracy - Ensures editing experience matches visual appearance of content Improved Textarea Sizing: - More reasonable height constraints (max 360px vs 400px) - Line-count based minimum height calculation (~24px per line) - Reduced excessive height for short content - Added both horizontal and vertical resize capability - Set minimum width constraint (200px) for better usability CSS Enhancements: - Changed resize from vertical-only to both directions - Added min-width constraint for better proportions - Improved overflow handling (auto vs overflow-y only) - Font properties use inherit with JavaScript override Technical Improvements: - Better content height calculation using actual line count - Proper handling of edge cases in markdown conversion - Maintained smooth transitions while fixing sizing logic - Preserved all existing functionality while fixing issues These fixes ensure that: - Headings preserve their # markers when edited - Font sizes match the original content being edited - Textarea dimensions are proportional and user-controllable - Markdown structure roundtrips accurately 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 22:27:19 +02:00
if (index > 0 && markdown.trim()) {
markdown += '\\n\\n';
}
feat: enhance empty line preservation and automatic paragraph separation Implemented sophisticated paragraph handling for the markdown editor: Enhanced HTML-to-Markdown Conversion: - Replaced simple tag stripping with proper structural parsing - Preserves formatting for headers, emphasis, code, blockquotes - Maintains paragraph separation with proper spacing - Handles nested elements and mixed content correctly Dynamic Section Splitting: - Detects paragraph breaks (double newlines) when editing - Automatically creates separate editable sections for each paragraph - Enables independent editing of logically separate content - Maintains proper section indexing with sub-identifiers Visual Enhancements: - Added green styling for edited sections to distinguish from originals - Subtle borders and backgrounds indicate modified content - Hover effects provide clear feedback on editable areas Technical Improvements: - Enhanced blur handler to detect multiple paragraphs - Smart wrapper creation for single vs. multi-paragraph content - Proper DOM manipulation for section insertion and replacement - Preserves editing state and section relationships Benefits: - Empty lines between paragraphs are preserved accurately - Text separated by empty lines becomes independently editable - Better content organization and editing granularity - Improved user experience with clear visual feedback This resolves the empty line swallowing issue and provides intuitive paragraph-level editing that matches user expectations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 21:35:53 +02:00
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;
}
"""