Files
markitect-main/markitect/clean_document_manager.py
tegwick 69aea1ada7 refactor(version): separate version and release commands
`markitect version` now prints a clean version string (Unix style),
with -v for commit/branch/dirty. `markitect release` shows detailed
development status: commits since tag, local changes, upstream
divergence. No overlap between the two commands.

Replaces get_version_info()/get_release_info() with get_version()
and get_release_status(). Drops yaml output format from release
(json + text sufficient).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:49:14 +01:00

1670 lines
71 KiB
Python

"""
Clean Document Manager - Simplified version with only clean editor support
"""
import json
import re
from pathlib import Path
from typing import Dict, Any, Optional
class CleanDocumentManager:
"""
Simplified document manager that only supports the clean editor implementation.
All legacy code has been removed for clarity and maintainability.
"""
def __init__(self, db_manager=None):
self.db_manager = db_manager
def store_document(self, file_path: str, content: str, ast: list = None, front_matter: dict = None):
"""Store a document in the database."""
if self.db_manager:
from pathlib import Path
filename = Path(file_path).name
return self.db_manager.store_markdown_file(filename, content)
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, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, nodogtag: bool = False,
image_max_width: str = '12cm', image_max_height: str = '20cm') -> Dict[str, Any]:
"""
Render a markdown file to HTML with optional clean editing capabilities.
"""
input_path = Path(input_file)
output_path = Path(output_file)
if not input_path.exists():
raise FileNotFoundError(f"Input file not found: {input_file}")
# Read markdown content
raw_markdown_content = input_path.read_text(encoding='utf-8')
# Process base64 images - relocate payloads to document end
markdown_content, base64_references = self._process_base64_images(raw_markdown_content)
# Extract title from markdown (first h1 heading)
title = self._extract_title_from_markdown(markdown_content)
# Get original filename without extension
original_filename = input_path.stem
# Get version information
version_info = self._get_version_info()
# Generate HTML content
html_content = self._generate_html_template(
markdown_content=markdown_content,
title=title,
css=css,
template=template,
edit_mode=edit_mode,
insert_mode=insert_mode,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
original_filename=original_filename,
version_info=version_info,
nodogtag=nodogtag,
image_max_width=image_max_width,
image_max_height=image_max_height,
base64_references=base64_references
)
# Write HTML file
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(html_content, encoding='utf-8')
return {
'success': True,
'input_file': str(input_path),
'output_file': str(output_path),
'edit_mode': edit_mode,
'editor_theme': editor_theme
}
def _extract_title_from_markdown(self, markdown_content: str) -> str:
"""Extract title from first h1 heading in markdown."""
match = re.search(r'^#\s+(.+)', markdown_content, re.MULTILINE)
if match:
return match.group(1).strip()
return "Markdown Document"
def _process_base64_images(self, markdown_content: str) -> tuple:
"""
Process base64 encoded images in markdown content.
- Extracts base64 image data URLs
- Replaces them with reference links
- Returns processed content and reference mapping
Returns:
tuple: (processed_markdown, base64_references_dict)
"""
import re
import uuid
# Pattern to match base64 image data URLs
base64_pattern = r'!\[([^\]]*)\]\(data:image/([^;]+);base64,([^)]+)\)'
base64_references = {}
reference_definitions = []
processed_content = markdown_content
# Find all base64 images
matches = list(re.finditer(base64_pattern, markdown_content))
for i, match in enumerate(matches):
alt_text = match.group(1)
image_type = match.group(2) # png, jpeg, svg+xml, etc.
base64_data = match.group(3)
# Generate a unique reference ID
ref_id = f"base64-image-{i+1}"
# Store the mapping
base64_references[ref_id] = {
'alt': alt_text,
'type': image_type,
'data': base64_data,
'full_data_url': f"data:image/{image_type};base64,{base64_data}"
}
# Replace the inline base64 with reference
original_match = match.group(0)
reference_link = f"![{alt_text}][{ref_id}]"
processed_content = processed_content.replace(original_match, reference_link, 1)
# Create reference definition for the end of document
reference_definitions.append(f"[{ref_id}]: data:image/{image_type};base64,{base64_data}")
# Add reference definitions to the end of the document if any base64 images were found
if reference_definitions:
# Ensure there's a blank line before the references
if not processed_content.endswith('\n\n'):
if processed_content.endswith('\n'):
processed_content += '\n'
else:
processed_content += '\n\n'
# Add a comment to indicate the base64 reference section
processed_content += "<!-- Base64 Image References -->\n"
processed_content += '\n'.join(reference_definitions) + '\n'
return processed_content, base64_references
def _get_version_info(self) -> dict:
"""Get repository name and version information."""
from .__version__ import get_version
return {
'repo_name': 'Markitect',
'version': get_version(),
'git_info': '',
}
def _get_template_css(self, template: str = None, image_max_width: str = '12cm', image_max_height: str = '20cm') -> str:
"""Generate layered theme CSS styles."""
# Import layered theme functions
from markitect.plugins.builtin.markdown_commands import (
parse_theme_string, combine_theme_properties, TEMPLATE_STYLES
)
# Handle layered themes or fall back to legacy
if template and ',' in template:
# New layered theme system
theme_list = parse_theme_string(template)
combined_props = combine_theme_properties(theme_list)
return self._generate_layered_css(combined_props, image_max_width, image_max_height)
else:
# Legacy single theme or fallback
if not template or template not in TEMPLATE_STYLES:
# Use default layered themes or the specified theme
theme_list = parse_theme_string(template or 'basic')
combined_props = combine_theme_properties(theme_list)
return self._generate_layered_css(combined_props, image_max_width, image_max_height)
else:
# Legacy theme - convert to layered
theme_list = parse_theme_string(template)
combined_props = combine_theme_properties(theme_list)
return self._generate_layered_css(combined_props, image_max_width, image_max_height)
def _generate_layered_css(self, properties: dict, image_max_width: str = '12cm', image_max_height: str = '20cm') -> str:
"""Generate CSS from combined theme properties."""
# Set defaults for missing properties (properties override defaults)
defaults = {
'body_background': '#ffffff',
'body_color': '#333333',
'font_family': '-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif',
'max_width': '800px',
'heading_color': '#333333', # Use same as body color by default
'heading_style': 'simple',
'text_align': 'left',
'code_background': '#f6f8fa',
'code_color': '#333333',
'border_color': '#d0d7de',
'blockquote_border': '#dfe2e5',
'blockquote_color': '#6a737d',
'table_border': '#d0d7de',
'table_header_bg': '#f6f8fa',
'accent_color': None,
'secondary_color': None
}
# Merge defaults first, then override with theme properties
props = {**defaults, **properties}
# Base CSS
base_css = f"""
body {{
font-family: {props['font_family']};
max-width: {props['max_width']};
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
color: {props['body_color']};
background-color: {props['body_background']};
}}
#markdown-content {{
min-height: 200px;
}}"""
# Heading styles
heading_font_style = ""
if 'heading_font_family' in props and props['heading_font_family']:
heading_font_style = f"font-family: {props['heading_font_family']};"
heading_css = ""
if props['heading_style'] == 'underlined':
heading_css = f"""
h1, h2, h3, h4, h5, h6 {{
color: {props['heading_color']};
{heading_font_style}
border-bottom: 1px solid {props['border_color']};
padding-bottom: 0.3em;
}}"""
elif props['heading_style'] == 'centered':
heading_css = f"""
h1, h2, h3, h4, h5, h6 {{
color: {props['heading_color']};
{heading_font_style}
margin-top: 2rem;
margin-bottom: 1rem;
}}
h1 {{
text-align: center;
font-size: 2.2em;
border-bottom: 2px solid {props['heading_color']};
padding-bottom: 0.5rem;
}}"""
else: # simple
heading_css = f"""
h1, h2, h3, h4, h5, h6 {{
color: {props['heading_color']};
{heading_font_style}
}}"""
# Text alignment
text_css = ""
if props['text_align'] == 'justify':
text_css = """
p {
text-align: justify;
margin-bottom: 1.2rem;
}"""
# Element styling
element_css = f"""
pre {{
background-color: {props['code_background']};
color: {props['code_color']};
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid {props['border_color']};
}}
code {{
background-color: {props['code_background']};
color: {props['code_color']};
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}}
pre code {{
background: none;
padding: 0;
}}
blockquote {{
border-left: 4px solid {props['blockquote_border']};
margin: 0;
padding-left: 1rem;
color: {props['blockquote_color']};
}}
table {{
font-size: 0.85em;
border-collapse: collapse;
margin: 1rem 0;
width: 100%;
border: 1px solid {props['table_border']};
}}
th, td {{
font-size: inherit;
border: 1px solid {props['table_border']};
padding: 0.5rem;
text-align: left;
}}
th {{
background-color: {props['table_header_bg']};
font-weight: 600;
}}"""
# Link styling
link_css = ""
if props.get('link_color'):
link_css = f"""
a {{
color: {props['link_color']};
text-decoration: underline;
}}"""
if props.get('link_hover_color'):
link_css += f"""
a:hover {{
color: {props['link_hover_color']};
}}"""
else:
link_css += """
a:hover {
opacity: 0.8;
}"""
# Branding accents (if specified and no link_color already set)
accent_css = ""
if props.get('accent_color') and not props.get('link_color'):
accent_css = f"""
a {{
color: {props['accent_color']};
}}
a:hover {{
opacity: 0.8;
}}"""
# UI theme styling for editor interface elements
ui_css = ""
if props.get('editor_panel_bg'):
ui_css = f"""
.markitect-edit-mode .ui-edit-floater-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-floater-header {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-floater-header h3 {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-inline-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 2px 8px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
padding: 12px;
margin: 8px 0;
}}
.markitect-edit-mode .ui-edit-button {{
background: {props.get('editor_button_bg', '#ffffff')};
color: {props.get('editor_text_color', '#212529')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
min-width: 70px;
font-weight: 500;
transition: all 0.2s;
}}
.markitect-edit-mode .ui-edit-button:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-edit-mode .ui-edit-button:active,
.markitect-edit-mode .ui-edit-button.active {{
background: {props.get('editor_button_active', '#dee2e6')};
}}
.markitect-edit-mode .ui-edit-button-accept {{
background: {props.get('editor_accept_bg', '#4caf50')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-accept:hover {{
background: {props.get('editor_accept_hover', '#388e3c')};
}}
.markitect-edit-mode .ui-edit-button-cancel {{
background: {props.get('editor_cancel_bg', '#f44336')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-cancel:hover {{
background: {props.get('editor_cancel_hover', '#d32f2f')};
}}
.markitect-edit-mode .ui-edit-button-reset {{
background: {props.get('editor_reset_bg', '#ff9800')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-reset:hover {{
background: {props.get('editor_reset_hover', '#f57c00')};
}}
.markitect-edit-mode .ui-edit-button-secondary {{
background: {props.get('editor_secondary_bg', '#6c757d')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-secondary:hover {{
background: {props.get('editor_secondary_hover', '#545b62')};
}}
.markitect-edit-mode .ui-edit-section-frame {{
border: 2px solid {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))};
box-shadow: 0 0 0 3px {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}33;
}}
.markitect-edit-mode .ui-edit-textarea {{
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
color: {props.get('editor_text_color', '#212529')};
background: {props.get('editor_button_bg', '#ffffff')};
}}
.markitect-edit-mode .ui-edit-textarea:focus {{
border-color: {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))};
box-shadow: 0 0 0 2px {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}33;
}}
.markitect-edit-mode .ui-edit-modal-overlay {{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}}
.markitect-edit-mode .ui-edit-modal-overlay.active {{
opacity: 1;
visibility: visible;
}}
.markitect-edit-mode .ui-edit-modal {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 8px 32px {props.get('editor_shadow', 'rgba(0,0,0,0.2)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
width: 90%;
overflow: hidden;
transform: scale(0.9) translateY(-20px);
transition: transform 0.3s;
}}
.markitect-edit-mode .ui-edit-modal-overlay.active .ui-edit-modal {{
transform: scale(1) translateY(0);
}}
.markitect-edit-mode .ui-edit-modal-header {{
padding: 20px 24px 16px;
border-bottom: 1px solid {props.get('editor_panel_border', '#dee2e6')};
display: flex;
justify-content: space-between;
align-items: center;
}}
.markitect-edit-mode .ui-edit-modal-title {{
margin: 0;
font-size: 18px;
font-weight: 600;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-modal-close {{
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: {props.get('editor_text_color', '#212529')};
padding: 4px;
border-radius: 4px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}}
.markitect-edit-mode .ui-edit-modal-close:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-edit-mode .ui-edit-modal-body {{
padding: 20px 24px;
overflow-y: auto;
max-height: 60vh;
}}
.markitect-edit-mode .ui-edit-modal-content {{
white-space: pre-line;
line-height: 1.5;
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
}}
.markitect-edit-mode .ui-edit-modal-section {{
margin-bottom: 16px;
}}
.markitect-edit-mode .ui-edit-modal-section:last-child {{
margin-bottom: 0;
}}
.markitect-edit-mode .ui-edit-modal-section-title {{
font-weight: 600;
margin-bottom: 8px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-modal-footer {{
padding: 16px 24px 20px;
border-top: 1px solid {props.get('editor_panel_border', '#dee2e6')};
text-align: right;
}}
/* Confirmation Dialog Styles */
.markitect-edit-mode .ui-edit-confirmation-modal {{
max-width: 500px;
}}
.markitect-edit-mode .ui-edit-confirmation-content {{
font-size: 16px;
line-height: 1.5;
margin-bottom: 24px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-confirmation-warning {{
background: {props.get('editor_warning_bg', '#fff3cd')};
border: 1px solid {props.get('editor_warning_border', '#ffeaa7')};
color: {props.get('editor_warning_text', '#856404')};
padding: 12px 16px;
border-radius: 6px;
margin: 16px 0;
font-size: 14px;
}}
.markitect-edit-mode .ui-edit-confirmation-buttons {{
display: flex;
gap: 12px;
justify-content: flex-end;
}}
.markitect-edit-mode .ui-edit-button-confirm {{
background: {props.get('editor_danger_button', '#dc3545')};
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}}
.markitect-edit-mode .ui-edit-button-confirm:hover {{
background: {props.get('editor_danger_button_hover', '#c82333')};
transform: translateY(-1px);
}}
.markitect-edit-mode .ui-edit-button-confirm:active {{
transform: translateY(0);
}}
.markitect-edit-mode .ui-edit-button-confirm:focus {{
outline: 2px solid {props.get('editor_focus_color', '#007bff')};
outline-offset: 2px;
}}
.markitect-edit-mode .ui-edit-button-cancel {{
background: {props.get('editor_secondary_button', '#6c757d')};
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}}
.markitect-edit-mode .ui-edit-button-cancel:hover {{
background: {props.get('editor_secondary_button_hover', '#545b62')};
transform: translateY(-1px);
}}
.markitect-edit-mode .ui-edit-button-cancel:active {{
transform: translateY(0);
}}
.markitect-edit-mode .ui-edit-button-cancel:focus {{
outline: 2px solid {props.get('editor_focus_color', '#007bff')};
outline-offset: 2px;
}}
/* Document Scroll Indicators */
.ui-scroll-indicator {{
position: fixed;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 30px;
background: {props.get('editor_panel_bg', '#f8f9fa')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.2s ease, background-color 0.2s ease;
z-index: 1000;
box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.15)')};
}}
.ui-scroll-indicator:hover {{
transform: translateX(-50%) scale(1.05);
}}
.ui-scroll-indicator:not(.disabled):hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.ui-scroll-indicator.active {{
opacity: 0.9;
visibility: visible;
}}
.ui-scroll-indicator.disabled {{
background: {props.get('editor_button_active', '#dee2e6')};
cursor: not-allowed;
opacity: 0.6;
}}
.ui-scroll-indicator.disabled:hover {{
transform: translateX(-50%);
background: {props.get('editor_button_active', '#dee2e6')};
}}
.ui-scroll-indicator-up {{
top: 20px;
}}
.ui-scroll-indicator-down {{
bottom: 20px;
}}
.ui-scroll-indicator::before {{
content: '';
width: 0;
height: 0;
border-style: solid;
transition: border-color 0.2s ease;
}}
.ui-scroll-indicator-up::before {{
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 12px solid {props.get('editor_text_color', '#212529')};
}}
.ui-scroll-indicator-down::before {{
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 12px solid {props.get('editor_text_color', '#212529')};
}}
.ui-scroll-indicator.disabled.ui-scroll-indicator-up::before {{
border-bottom-color: {props.get('editor_secondary_button', '#6c757d')};
}}
.ui-scroll-indicator.disabled.ui-scroll-indicator-down::before {{
border-top-color: {props.get('editor_secondary_button', '#6c757d')};
}}
/* Insert Mode Specific Styles */
.markitect-insert-mode .ui-edit-floater-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-floater-header {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-floater-header h3 {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-inline-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 2px 8px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
padding: 12px;
margin: 8px 0;
}}
.markitect-insert-mode .ui-insert-protected-panel {{
border-left: 4px solid #ff9800;
}}
.markitect-insert-mode .ui-insert-heading-display {{
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 14px;
font-weight: bold;
padding: 8px 12px;
background: {props.get('editor_warning_bg', '#fff3cd')};
border: 1px solid {props.get('editor_warning_border', '#ffeaa7')};
border-radius: 4px;
border-left: 4px solid #007bff;
color: {props.get('editor_warning_text', '#856404')};
margin-bottom: 8px;
}}
.markitect-insert-mode .ui-insert-content-editor {{
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
color: {props.get('editor_text_color', '#212529')};
background: {props.get('editor_button_bg', '#ffffff')};
}}
.markitect-insert-mode .ui-insert-content-editor:focus {{
border-color: {props.get('editor_focus_color', '#007bff')};
box-shadow: 0 0 0 2px {props.get('editor_focus_color', '#007bff')}33;
}}
.markitect-insert-mode .ui-edit-button {{
background: {props.get('editor_button_bg', '#ffffff')};
color: {props.get('editor_text_color', '#212529')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
min-width: 70px;
font-weight: 500;
transition: all 0.2s;
}}
.markitect-insert-mode .ui-edit-button:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-insert-mode .ui-edit-button:active,
.markitect-insert-mode .ui-edit-button.active {{
background: {props.get('editor_button_active', '#dee2e6')};
}}
.markitect-insert-mode .ui-edit-button-accept {{
background: #4caf50;
color: white;
}}
.markitect-insert-mode .ui-edit-button-accept:hover {{
background: #388e3c;
}}
.markitect-insert-mode .ui-edit-button-cancel {{
background: #f44336;
color: white;
}}
.markitect-insert-mode .ui-edit-button-cancel:hover {{
background: #d32f2f;
}}
.markitect-insert-mode .ui-edit-button-reset {{
background: #ff9800;
color: white;
}}
.markitect-insert-mode .ui-edit-button-reset:hover {{
background: #f57c00;
}}
.markitect-insert-mode .ui-edit-section-frame {{
border: 2px solid {props.get('editor_focus_color', '#007bff')};
box-shadow: 0 0 0 3px {props.get('editor_focus_color', '#007bff')}33;
}}
/* Modal Overlay and Dialog Styles for Insert Mode */
.markitect-insert-mode .ui-edit-modal-overlay {{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}}
.markitect-insert-mode .ui-edit-modal-overlay.active {{
opacity: 1;
visibility: visible;
}}
.markitect-insert-mode .ui-edit-modal {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 8px 32px {props.get('editor_shadow', 'rgba(0,0,0,0.2)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
width: 90%;
overflow: hidden;
transform: scale(0.9) translateY(-20px);
transition: transform 0.3s;
}}
.markitect-insert-mode .ui-edit-modal-overlay.active .ui-edit-modal {{
transform: scale(1) translateY(0);
}}
.markitect-insert-mode .ui-edit-modal-header {{
padding: 20px 24px 16px;
border-bottom: 1px solid {props.get('editor_panel_border', '#dee2e6')};
display: flex;
justify-content: space-between;
align-items: center;
}}
.markitect-insert-mode .ui-edit-modal-title {{
margin: 0;
font-size: 18px;
font-weight: 600;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-modal-close {{
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: {props.get('editor_text_color', '#212529')};
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}}
.markitect-insert-mode .ui-edit-modal-close:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-insert-mode .ui-edit-modal-body {{
padding: 20px 24px;
max-height: 400px;
overflow-y: auto;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-modal-section {{
margin-bottom: 8px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-modal-footer {{
padding: 16px 24px 20px;
border-top: 1px solid {props.get('editor_panel_border', '#dee2e6')};
text-align: right;
}}
/* Confirmation Dialog Styles for Insert Mode */
.markitect-insert-mode .ui-edit-confirmation-modal {{
max-width: 500px;
}}
.markitect-insert-mode .ui-edit-confirmation-content {{
font-size: 16px;
line-height: 1.5;
margin-bottom: 24px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-confirmation-warning {{
background: {props.get('editor_warning_bg', '#fff3cd')};
color: {props.get('editor_warning_text', '#856404')};
border: 1px solid {props.get('editor_warning_border', '#ffeaa7')};
border-radius: 6px;
padding: 12px 16px;
margin: 16px 0;
font-size: 14px;
line-height: 1.4;
}}
.markitect-insert-mode .ui-edit-confirmation-buttons {{
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
}}
.markitect-insert-mode .ui-edit-button-confirm {{
background: #dc3545;
color: white;
border: 1px solid #dc3545;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}}
.markitect-insert-mode .ui-edit-button-confirm:hover {{
background: #c82333;
border-color: #bd2130;
}}
.markitect-insert-mode .ui-edit-button-cancel {{
background: {props.get('editor_button_bg', '#ffffff')};
color: {props.get('editor_text_color', '#212529')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}}
.markitect-insert-mode .ui-edit-button-cancel:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
"""
# Image size CSS with configurable limits
image_css = f"""
img {{
max-width: {image_max_width};
max-height: {image_max_height};
height: auto;
display: block;
margin: 1rem auto;
}}"""
return f"<style>{base_css}{heading_css}{text_css}{element_css}{link_css}{accent_css}{ui_css}{image_css}</style>"
def _get_legacy_template_css(self, template: str, image_max_width: str = '12cm', image_max_height: str = '20cm') -> str:
"""Legacy CSS generation - kept for backward compatibility."""
# Import template styles
from markitect.plugins.builtin.markdown_commands import TEMPLATE_STYLES
# Use basic as default if no template specified
if not template or template not in TEMPLATE_STYLES:
template = 'basic'
style_config = TEMPLATE_STYLES[template]
# Base CSS that's common to all templates
base_css = f"""
body {{
font-family: {style_config['font_family']};
max-width: {style_config['max_width']};
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
color: {style_config['body_color']};
}}
#markdown-content {{
min-height: 200px;
}}"""
# Convert legacy template config to layered format
legacy_config = TEMPLATE_STYLES[template]
layered_props = {
'font_family': legacy_config['font_family'],
'max_width': legacy_config['max_width'],
'body_color': legacy_config['body_color'],
}
return self._generate_layered_css(layered_props, image_max_width, image_max_height)
def _generate_html_template(self, markdown_content: str, title: str, css: str = None, template: str = None,
edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None, nodogtag: bool = False,
image_max_width: str = '12cm', image_max_height: str = '20cm', base64_references: dict = None) -> str:
"""Generate clean HTML template using external template file."""
# Check if edit/insert mode components are available
if edit_mode or insert_mode:
mode_name = "insert mode" if insert_mode else "edit mode"
# Check if required components exist
components_to_check = [
'js/core/section-manager.js',
'js/components/debug-panel.js',
'js/components/document-controls.js',
'js/components/dom-renderer.js'
]
base_path = Path(__file__).parent / 'static'
missing_components = []
for component_path in components_to_check:
script_path = base_path / component_path
if not script_path.exists():
missing_components.append(component_path)
if missing_components:
error_msg = f"""
⚠️ WARNING: {mode_name.title()} requested but some components are missing!
Missing components:
{chr(10).join(f' - {comp}' for comp in missing_components)}
The system will attempt to load {mode_name} but some functionality may be broken.
RECOMMENDATIONS:
1. Restore missing components from git: git show HEAD:markitect/static/js/...
2. Use static mode instead: Remove --edit or --insert flag
3. Check if all editor components were properly restored
FILE: {original_filename}
MODE REQUESTED: {mode_name}
MISSING: {len(missing_components)} components
"""
print(error_msg)
# In strict mode, fail fast for missing components
if self._should_fail_fast():
raise FileNotFoundError(f"{mode_name.title()} components missing: {', '.join(missing_components)}")
else:
print(f"{mode_name.title()} components found - proceeding with interactive editing")
# Add dogtag to markdown content if not disabled
if not nodogtag:
import datetime
import getpass
now = datetime.datetime.now()
datetime_str = now.strftime("%Y-%m-%d %H:%M:%S")
try:
username = getpass.getuser()
except:
username = "user"
# Create username link only for 'worsch', otherwise just show username
if username == 'worsch':
username_link = f'<a href="https://coulomb.social/open/worsch" target="_blank">{username}</a>'
else:
username_link = username
dogtag = f'\n\n---\n*-- html from markdown by <a href="https://coulomb.social/open/MarkiTect" target="_blank">MarkiTect</a> on {datetime_str} by {username_link}*'
markdown_content_with_dogtag = markdown_content + dogtag
else:
markdown_content_with_dogtag = markdown_content
dogtag = ""
# Choose template based on mode
if edit_mode or insert_mode:
return self._generate_clean_edit_mode_html(
markdown_content=markdown_content,
markdown_content_with_dogtag=markdown_content_with_dogtag,
dogtag=dogtag,
title=title,
css=css,
template=template,
edit_mode=edit_mode,
insert_mode=insert_mode,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
original_filename=original_filename,
version_info=version_info,
image_max_width=image_max_width,
image_max_height=image_max_height,
base64_references=base64_references
)
# Legacy edit mode (will be removed)
if False: # edit_mode or insert_mode:
# Use the original embedded template for edit/insert modes
mode_class = 'markitect-edit-mode' if edit_mode else 'markitect-insert-mode'
# Convert data to JavaScript-safe strings
js_markdown_content = json.dumps(markdown_content)
js_markdown_content_with_dogtag = json.dumps(markdown_content_with_dogtag)
js_dogtag_content = json.dumps(dogtag)
js_base64_references = json.dumps(base64_references or {})
# Get editor configuration
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.5.0.dev"
if edit_mode:
editor_config = f"""
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
mode: 'edit',
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: false,
sections: true,
originalFilename: '{original_filename}',
version: '{version_str}',
repoName: '{version_info['repo_name'] if version_info else 'Markitect'}'
}};
window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
else: # insert_mode
editor_config = f"""
const MARKITECT_INSERT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
mode: 'insert',
restrictedHeadingLevels: [1, 2, 3],
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: false,
sections: true,
originalFilename: '{original_filename}',
version: '{version_str}',
repoName: '{version_info['repo_name'] if version_info else 'Markitect'}'
}};
window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
# Get editor scripts
editor_scripts = self._get_clean_editor_scripts()
# Generate CSS
css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css
# Use the original embedded template structure
template_content = """<!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}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
onload="window.markitectMarkedLoaded = true"
onerror="window.markitectMarkedError = true"></script>
</head>
<body class="{mode_class}">
<div id="markdown-content"></div>
<script>
const markdownContent = {js_markdown_content};
const markdownContentWithDogtag = {js_markdown_content_with_dogtag};
const dogtagContent = {js_dogtag_content};
window.markitectBase64References = {js_base64_references};
{editor_config}
{editor_scripts}
// Always render content first (graceful degradation)
document.addEventListener('DOMContentLoaded', function() {
console.log("🎯 Rendering content in {mode_type} mode...");
// Initialize edit/insert capabilities first (always needed)
if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) ||
(typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {
const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit';
console.log('🚀 Initializing clean ' + mode + ' capabilities...');
try {
console.log("Creating clean editor instance...");
initializeCleanEditor();
if (mode === 'insert') {
console.log("✅ Clean insert mode active - click any section to edit (headings 1-3 protected)");
} else {
console.log("✅ Clean edit mode active - click any section to edit");
}
} catch (error) {
console.error('❌ Clean ' + mode + ' mode failed to initialize:', error);
}
}
// Check if modular components are being used for content rendering
if (typeof SectionManager !== 'undefined') {
console.log("✓ Modular components detected - skipping fallback content rendering");
console.log("✓ Content will be rendered by modular architecture");
return;
}
const contentDiv = document.getElementById('markdown-content');
// Step 1: Ensure content is always displayed (fallback for non-modular mode)
if (contentDiv) {
if (typeof marked !== 'undefined') {
try {
const html = marked.parse(markdownContentWithDogtag);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
contentDiv.innerHTML = htmlWithTargetBlank;
console.log("✓ Content rendered successfully");
} catch (error) {
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
console.error("Content rendered with errors");
console.error("Markdown parsing failed:", error.message);
}
} 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");
console.warn("CDN library failed to load - using basic fallback rendering");
}
}
// Step 3: Initialize scroll indicators
try {
initializeScrollIndicators();
} catch (error) {
console.error("Scroll indicators failed to initialize:", error);
}
});
window.addEventListener('load', function() {
if (window.markitectMarkedError) {
console.error("CDN library failed to load - network or firewall blocking marked.js");
}
});
</script>
</body>
</html>"""
# Determine version string for template substitution
if version_info:
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}"
else:
version_str = "0.5.0.dev"
# Replace template placeholders (same as static mode)
html_template = template_content.replace('{title}', title)
html_template = html_template.replace('{version}', version_str)
# Replace JavaScript variables with properly escaped JSON
html_template = html_template.replace('{js_markdown_content}', js_markdown_content)
html_template = html_template.replace('{js_markdown_content_with_dogtag}', js_markdown_content_with_dogtag)
html_template = html_template.replace('{js_dogtag_content}', js_dogtag_content)
html_template = html_template.replace('{js_base64_references}', js_base64_references)
html_template = html_template.replace('{editor_config}', editor_config)
html_template = html_template.replace('{editor_scripts}', editor_scripts)
html_template = html_template.replace('{css_content}', css_content)
html_template = html_template.replace('{mode_class}', mode_class)
html_template = html_template.replace('{mode_type}', 'insert' if insert_mode else 'edit')
# No {content} placeholder in edit mode - content is handled by JavaScript
return html_template
else:
# Use external template for static viewing mode
template_path = Path(__file__).parent / 'templates' / 'document.html'
if not template_path.exists():
# Fallback to a minimal template if external template not found
template_content = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<meta name="generator" content="Markitect {version}">
</head>
<body>
<div id="markitect-content">{content}</div>
</body>
</html>"""
else:
template_content = template_path.read_text(encoding='utf-8')
# Determine version string
if version_info:
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}"
else:
version_str = "0.5.0.dev"
# Convert markdown to HTML (basic conversion)
try:
import markdown
html_content = markdown.markdown(markdown_content_with_dogtag, extensions=['extra', 'codehilite', 'toc'])
except ImportError:
# Fallback: simple line breaks and basic formatting
html_content = markdown_content_with_dogtag.replace('\n\n', '</p><p>').replace('\n', '<br>')
html_content = f'<p>{html_content}</p>'
# Generate or read CSS content
if css:
# If css is a file path, read it
css_path = Path(css)
if css_path.exists() and css_path.is_file():
css_content = f'<style>\n{css_path.read_text(encoding="utf-8")}\n</style>'
else:
# Assume it's raw CSS content
css_content = f'<style>\n{css}\n</style>'
else:
# Use template-based CSS generation
css_content = self._get_template_css(template, image_max_width, image_max_height)
# Replace template placeholders using safe string replacement
# This avoids conflicts with CSS curly braces
html_template = template_content.replace('{title}', title)
html_template = html_template.replace('{version}', version_str)
html_template = html_template.replace('{content}', html_content)
html_template = html_template.replace('{css_content}', css_content)
return html_template
def _generate_clean_edit_mode_html(self, markdown_content: str, markdown_content_with_dogtag: str, dogtag: str, title: str, css: str = None, template: str = None, edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None, image_max_width: str = '12cm', image_max_height: str = '20cm', base64_references: dict = None) -> str:
"""Generate clean HTML for edit mode using external script references like non-edit mode."""
# Use the fixed template that follows non-edit pattern
template_path = Path(__file__).parent / 'templates' / 'edit-mode-fixed.html'
if not template_path.exists():
raise FileNotFoundError(f"Fixed edit mode template not found: {template_path}")
template_content = template_path.read_text(encoding='utf-8')
# Generate or read CSS content
if css:
# If css is a file path, read it
css_path = Path(css)
if css_path.exists() and css_path.is_file():
css_content = f'<style>\n{css_path.read_text(encoding="utf-8")}\n</style>'
else:
# Assume it's raw CSS content
css_content = f'<style>\n{css}\n</style>'
else:
# Use template-based CSS generation
css_content = self._get_template_css(template, image_max_width, image_max_height)
# Create configuration object - ONLY dynamic data interface
config = {
'markdownContent': markdown_content,
'markdownContentWithDogtag': markdown_content_with_dogtag,
'dogtagContent': dogtag,
'mode': 'insert' if insert_mode else 'edit',
'theme': editor_theme,
'keyboardShortcuts': keyboard_shortcuts,
'autosave': False,
'sections': True,
'originalFilename': original_filename,
'base64References': base64_references or {}
}
# Add version info
if version_info:
config['version'] = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}"
config['repoName'] = version_info['repo_name']
else:
# Get version from CLI command as fallback
import subprocess
try:
result = subprocess.run(['markitect', '--version'], capture_output=True, text=True, timeout=5)
actual_version = result.stdout.strip() if result.returncode == 0 else '0.8.1.dev44+gf788ccdfd.d20251114'
except:
actual_version = '0.8.1.dev44+gf788ccdfd.d20251114'
config['version'] = f'Markitect v{actual_version}'
config['repoName'] = 'Markitect'
# Add insert mode specific config
if insert_mode:
config['restrictedHeadingLevels'] = [1, 2, 3]
# Convert config to JSON - This is the ONLY place Python data enters JavaScript
config_json = json.dumps(config, ensure_ascii=False, separators=(',', ':'))
# Mode class for body
mode_class = 'markitect-insert-mode' if insert_mode else 'markitect-edit-mode'
# Version string for template
version_str = config['version']
# Generate fallback content (like non-edit mode)
fallback_content = self._render_markdown_to_html(markdown_content_with_dogtag)
# Replace template placeholders - all safe static replacements
html_template = template_content.replace('{title}', title)
html_template = html_template.replace('{version}', version_str)
html_template = html_template.replace('{css_content}', css_content)
html_template = html_template.replace('{mode_class}', mode_class)
html_template = html_template.replace('{config_json}', config_json)
html_template = html_template.replace('{fallback_content}', fallback_content)
return html_template
def _render_markdown_to_html(self, markdown_content: str) -> str:
"""Render markdown to HTML for fallback content (same as non-edit mode)."""
try:
from markdown import markdown
# Use basic markdown rendering
html_content = markdown(markdown_content)
# Add target="_blank" to all links (same as non-edit mode)
import re
html_content = re.sub(
r'<a href="([^"]*)"([^>]*)>',
r'<a href="\1" target="_blank"\2>',
html_content
)
return html_content
except ImportError:
# Fallback if markdown not available
import html
lines = markdown_content.split('\n')
html_lines = []
for line in lines:
line = line.strip()
if line.startswith('# '):
html_lines.append(f'<h1>{html.escape(line[2:])}</h1>')
elif line.startswith('## '):
html_lines.append(f'<h2>{html.escape(line[3:])}</h2>')
elif line.startswith('### '):
html_lines.append(f'<h3>{html.escape(line[4:])}</h3>')
elif line:
html_lines.append(f'<p>{html.escape(line)}</p>')
return '\n'.join(html_lines)
def _should_fail_fast(self) -> bool:
"""
Determine if we should fail fast (development mode) or continue gracefully (production mode).
Fail fast in:
- Development environments (localhost, 127.0.0.1)
- When strict mode is enabled via environment variable
- When running in test environments
Continue gracefully in:
- Production environments
- When explicitly disabled via environment variable
"""
import os
# Check environment variables first
strict_env = os.getenv('MARKITECT_STRICT_MODE', '').lower()
if strict_env in ('true', '1', 'yes', 'on'):
return True
if strict_env in ('false', '0', 'no', 'off'):
return False
# Check if we're in a development environment
# This mimics the JavaScript strict mode detection
try:
import socket
hostname = socket.gethostname().lower()
if 'localhost' in hostname or hostname.startswith('127.') or 'dev' in hostname:
return True
except:
pass
# Check for test environment indicators
if any(env in os.environ for env in ['PYTEST_CURRENT_TEST', 'CI', 'CONTINUOUS_INTEGRATION', 'TESTING']):
return True
# Default to graceful handling in production
return False
def _get_clean_editor_scripts_backup(self) -> str:
"""Legacy method kept for reference - should not be used."""
# This method contained embedded JavaScript that has been moved to external files
return ""
def _get_clean_editor_scripts(self) -> str:
"""Load the modular editor JavaScript components from external files."""
from pathlib import Path
# Define the modular components to load in order
components = [
'js/core/debug-system.js',
'js/core/section-manager.js',
'js/components/debug-panel.js',
'js/components/document-controls.js',
'js/components/dom-renderer.js',
'js/controls/control-base.js',
'js/controls/contents-control.js',
'js/controls/status-control.js',
'js/controls/debug-control.js',
'js/controls/edit-control.js',
'js/main.js'
]
base_path = Path(__file__).parent / 'static'
combined_script = []
# Load each component
for component_path in components:
script_path = base_path / component_path
if script_path.exists():
try:
content = script_path.read_text(encoding='utf-8')
combined_script.append(f"// === {component_path} ===")
combined_script.append(content)
combined_script.append("") # Add blank line between components
except Exception as e:
print(f"Warning: Could not read {component_path}: {e}")
combined_script.append(f"console.error('Failed to load {component_path}');")
else:
print(f"Warning: {component_path} not found at {script_path}")
combined_script.append(f"console.error('{component_path} file not found');")
# Add initialization script to wire up the components
initialization_script = """
// === Missing Function Definitions ===
function initializeCleanEditor() {
console.log('✅ initializeCleanEditor: Modular components will handle initialization');
// This function was missing - the modular components handle initialization automatically
// No additional action needed here
}
function initializeScrollIndicators() {
console.log('✅ initializeScrollIndicators: Basic scroll indicators initialized');
// Simple scroll indicator implementation for document navigation
// This is a placeholder - can be enhanced later
}
// === Component Initialization ===
document.addEventListener('DOMContentLoaded', function() {
// Create container for the markdown content
const container = document.getElementById('markdown-content') || document.body;
// Initialize components
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
// Create document controls
documentControls.create();
// Step 4: Initialize modern Control-based architecture with compass positioning
console.log("🎛️ Initializing modern Control system with compass positioning...");
// ContentsControl (positioned upper left - nw)
const contentsControl = new ContentsControl();
contentsControl.control.config.position = 'nw'; // Upper left
contentsControl.createControl();
window.contentsControl = contentsControl;
// StatusControl (positioned right - e)
const statusControl = new StatusControl();
statusControl.control.config.position = 'e'; // Right
statusControl.createControl();
window.statusControl = statusControl;
// DebugControl (positioned lower right - se)
const debugControl = new DebugControl();
debugControl.control.config.position = 'se'; // Lower right
debugControl.createControl();
window.debugControl = debugControl;
// EditControl (positioned upper right - ne)
const editControl = new EditControl();
editControl.control.config.position = 'ne'; // Upper right
editControl.createControl();
window.editControl = editControl;
console.log("🎛️ Modern Control system initialized with compass positioning");
// Wire up event handlers
documentControls.setEventHandlers({
'save-document': () => {
console.log('Save document clicked');
try {
// Get current markdown content from section manager
const currentMarkdown = sectionManager.getDocumentMarkdown();
// Create filename with timestamp suffix following the established convention
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
// Extract original filename from config or use default
const originalFilename = window.editorConfig?.originalFilename || 'document';
const editedFilename = `${originalFilename}-edited-${timestamp}.md`;
// Create and download the file
const blob = new Blob([currentMarkdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = editedFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Log success to debug panel
debugPanel.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS');
console.log(`Document successfully saved as: ${editedFilename}`);
} catch (error) {
debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR');
console.error('Save error:', error);
}
},
'reset-all': () => {
console.log('Reset all clicked');
// Hide any open editors
domRenderer.hideCurrentEditor();
// Reset all sections to original state
const allSections = Array.from(sectionManager.sections.values());
allSections.forEach(section => {
section.resetToOriginal();
});
// Re-render all sections
domRenderer.renderAllSections(allSections);
debugPanel.addMessage(`Reset all sections to original state`, 'INFO');
},
'show-status': () => {
const status = sectionManager.getDocumentStatus();
alert(`Document Status:\\nTotal Sections: ${status.totalSections}\\nEditing Sections: ${status.editingSections}`);
},
'toggle-debug': () => {
debugPanel.toggle();
}
});
// Set up debug panel integration
sectionManager.on('sections-created', (data) => {
debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
});
sectionManager.on('edit-started', (data) => {
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
});
sectionManager.on('changes-accepted', (data) => {
debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
// Re-render the section to show updated content
const section = sectionManager.sections.get(data.sectionId);
if (section) {
const sectionElement = domRenderer.findSectionElement(data.sectionId);
if (sectionElement) {
const newElement = domRenderer.renderSection(section);
sectionElement.parentNode.replaceChild(newElement, sectionElement);
debugPanel.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO');
}
}
});
sectionManager.on('changes-cancelled', (data) => {
debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
});
// Initialize with markdown content
const markdownToRender = markdownContent || '';
if (markdownToRender.trim()) {
const sections = sectionManager.createSectionsFromMarkdown(markdownToRender);
domRenderer.renderAllSections(sections);
debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
} else {
debugPanel.addMessage('No markdown content to initialize', 'WARNING');
}
// Make components globally available for debugging
window.markitectComponents = {
sectionManager,
domRenderer,
debugPanel,
documentControls
};
console.log('Markitect modular editor initialized successfully');
});
"""
combined_script.append(initialization_script)
return '\n'.join(combined_script)