- Add resize functionality to all controls with hover-only visibility - Replace heading tags with control-title CSS class to prevent content confusion - Implement small circle resize handles positioned in lower-right corner - Add header-only toggle mode for space-efficient control management - Create independent IndexedDB-based debug system with selection filtering - Fix green button backgrounds in debug control (use neutral grey) - Add hover behavior for clean interface (resize handle and close button) - Support document structure scanning for targeted debugging - Enable drag positioning with 16-point compass system - Add persistent storage for debug messages across browser sessions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
3024 lines
134 KiB
Python
3024 lines
134 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_info
|
|
|
|
version_info = get_version_info()
|
|
|
|
# Transform to the format expected by the editor
|
|
return {
|
|
'repo_name': 'Markitect',
|
|
'version': version_info['full_version'],
|
|
'git_info': '' # Already included in full_version
|
|
}
|
|
|
|
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."""
|
|
|
|
# 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 = ""
|
|
|
|
# Pass original markdown content to editor (without dogtag for editing)
|
|
# But make dogtag available separately for protected display in editor
|
|
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 {})
|
|
|
|
# Handle CSS styles
|
|
css_content = ""
|
|
if css:
|
|
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:
|
|
css_content = f'<link rel="stylesheet" href="{css}">'
|
|
except Exception:
|
|
css_content = f'<link rel="stylesheet" href="{css}">'
|
|
|
|
# Generate template-specific CSS
|
|
default_css = self._get_template_css(template, image_max_width, image_max_height)
|
|
|
|
# Load clean editor JavaScript files
|
|
editor_scripts = ""
|
|
editor_config = ""
|
|
body_classes = ""
|
|
|
|
if edit_mode:
|
|
body_classes = ' class="markitect-edit-mode"'
|
|
|
|
# Configuration for clean editor
|
|
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.5.0.dev"
|
|
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'}'
|
|
}};
|
|
|
|
// Make config available globally
|
|
window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
|
|
elif insert_mode:
|
|
body_classes = ' class="markitect-insert-mode"'
|
|
|
|
# Configuration for insert mode editor
|
|
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.5.0.dev"
|
|
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'}'
|
|
}};
|
|
|
|
// Make config available globally
|
|
window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
|
|
|
|
# Load clean editor architecture for both edit and insert modes
|
|
if edit_mode or insert_mode:
|
|
editor_scripts = self._get_clean_editor_scripts()
|
|
else:
|
|
editor_scripts = ""
|
|
|
|
# Generate the complete HTML template
|
|
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}
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
|
onload="window.markitectMarkedLoaded = true"
|
|
onerror="window.markitectMarkedError = true"></script>
|
|
</head>
|
|
<body{body_classes}>
|
|
|
|
<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...");
|
|
|
|
// Check if modular components are being used
|
|
if (typeof SectionManager !== 'undefined') {{
|
|
console.log("✓ Modular components detected - skipping direct 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");
|
|
console.log('✓ Markdown 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 2: Initialize edit/insert capabilities if enabled
|
|
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);
|
|
}}
|
|
}}
|
|
|
|
// Step 3: Initialize document scroll indicators (always available)
|
|
try {{
|
|
initializeScrollIndicators();
|
|
}} catch (error) {{
|
|
console.error("Scroll indicators failed to initialize:", error);
|
|
}}
|
|
|
|
// Step 4: Define abstract Control class for UI controls
|
|
const Control = {{
|
|
// Abstract control properties
|
|
element: null,
|
|
isExpanded: false,
|
|
isHeaderOnly: false, // New state for header-only mode
|
|
isDragging: false,
|
|
isResizing: false, // New state for resizing mode
|
|
dragOffset: {{ x: 0, y: 0 }},
|
|
resizeStartSize: {{ width: 280, height: 'auto' }},
|
|
originalPosition: {{ top: '80px', left: '20px' }},
|
|
defaultSize: {{ width: 280, minWidth: 200, minHeight: 150 }},
|
|
|
|
// Configuration properties (to be overridden by subclasses)
|
|
config: {{
|
|
icon: '?',
|
|
title: 'Control',
|
|
className: 'control',
|
|
defaultContent: 'Template only',
|
|
ariaLabel: 'Control',
|
|
position: 'w' // Default compass position: west (middle-left)
|
|
}},
|
|
|
|
// Compass positioning system (top-aligned for proper expansion)
|
|
compassPositions: {{
|
|
// North positions (top)
|
|
'n': {{ top: '20px', left: '50%', transform: 'translateX(-50%)' }},
|
|
'nne': {{ top: '20px', left: '65%', transform: 'translateX(-50%)' }},
|
|
'ne': {{ top: '20px', right: '20px' }},
|
|
'ene': {{ top: '80px', right: '20px' }}, // Top-aligned instead of center
|
|
|
|
// East positions (right)
|
|
'e': {{ top: '50vh', right: '20px', transform: 'translateY(-20px)' }}, // Anchor at icon level
|
|
'ese': {{ top: 'calc(65vh - 20px)', right: '20px' }}, // Top-aligned
|
|
'se': {{ bottom: '20px', right: '20px' }},
|
|
'sse': {{ bottom: '20px', right: '35%', transform: 'translateX(50%)' }},
|
|
|
|
// South positions (bottom)
|
|
's': {{ bottom: '20px', left: '50%', transform: 'translateX(-50%)' }},
|
|
'ssw': {{ bottom: '20px', left: '35%', transform: 'translateX(-50%)' }},
|
|
'sw': {{ bottom: '20px', left: '20px' }},
|
|
'wsw': {{ bottom: '80px', left: '20px' }}, // Top-aligned instead of center
|
|
|
|
// West positions (left) - top-aligned for proper expansion
|
|
'w': {{ top: '50vh', left: '20px', transform: 'translateY(-20px)' }}, // Anchor at icon level
|
|
'wnw': {{ top: '80px', left: '20px' }}, // Top-aligned instead of center
|
|
'nw': {{ top: '20px', left: '20px' }},
|
|
'nnw': {{ top: '20px', left: '35%', transform: 'translateX(-50%)' }}
|
|
}},
|
|
|
|
// Get expansion direction based on compass position
|
|
getExpansionDirection: function() {{
|
|
const pos = this.config.position;
|
|
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
|
|
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
|
|
|
|
return {{
|
|
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
|
|
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
|
|
}};
|
|
}},
|
|
|
|
// Calculate position styles based on compass direction
|
|
getPositionStyles: function() {{
|
|
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
|
|
return {{
|
|
position: 'fixed',
|
|
top: compassPos.top || 'auto',
|
|
right: compassPos.right || 'auto',
|
|
bottom: compassPos.bottom || 'auto',
|
|
left: compassPos.left || 'auto',
|
|
transform: compassPos.transform || 'none',
|
|
zIndex: 1000
|
|
}};
|
|
}},
|
|
|
|
// Abstract methods (to be implemented by subclasses)
|
|
buildContent: function() {{
|
|
const content = this.element.querySelector('.control-content');
|
|
content.innerHTML = `<p style="padding: 1rem; color: #666;">${{this.config.defaultContent}}</p>`;
|
|
}},
|
|
|
|
// Concrete methods (shared by all controls)
|
|
createControl: function() {{
|
|
console.log(`🎛️ Creating ${{this.config.title}} control...`);
|
|
|
|
this.element = document.createElement('div');
|
|
this.element.className = this.config.className;
|
|
this.element.innerHTML = `
|
|
<button class="control-toggle" aria-label="${{this.config.ariaLabel}}">${{this.config.icon}}</button>
|
|
<div class="control-panel" style="display: none;">
|
|
<div class="control-header">
|
|
<span class="control-icon">${{this.config.icon}}</span>
|
|
<span class="control-title">${{this.config.title}}</span>
|
|
<button class="control-close">✕</button>
|
|
</div>
|
|
<div class="control-content">Loading...</div>
|
|
</div>
|
|
`;
|
|
|
|
// Position using compass direction
|
|
const positionStyles = this.getPositionStyles();
|
|
this.element.style.cssText = `
|
|
position: ${{positionStyles.position}};
|
|
top: ${{positionStyles.top}};
|
|
right: ${{positionStyles.right}};
|
|
bottom: ${{positionStyles.bottom}};
|
|
left: ${{positionStyles.left}};
|
|
transform: ${{positionStyles.transform}};
|
|
z-index: ${{positionStyles.zIndex}};
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border: 1px solid #e1e5e9;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
backdrop-filter: blur(8px);
|
|
width: 40px;
|
|
transition: all 0.3s ease;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
`;
|
|
|
|
// Store original position for reset
|
|
this.originalPosition = {{
|
|
top: positionStyles.top,
|
|
right: positionStyles.right,
|
|
bottom: positionStyles.bottom,
|
|
left: positionStyles.left,
|
|
transform: positionStyles.transform
|
|
}};
|
|
|
|
// Style toggle button
|
|
const toggleBtn = this.element.querySelector('.control-toggle');
|
|
toggleBtn.style.cssText = `
|
|
width: 100%;
|
|
height: 40px;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
color: #666;
|
|
transition: color 0.2s ease;
|
|
`;
|
|
|
|
// Handle click to build content on-demand
|
|
toggleBtn.addEventListener('click', () => {{
|
|
if (this.isExpanded) {{
|
|
this.collapse();
|
|
}} else {{
|
|
console.log(`🎛️ ${{this.config.title}} toggle clicked - building content...`);
|
|
this.buildContent();
|
|
}}
|
|
}});
|
|
|
|
// Close button handler
|
|
const closeBtn = this.element.querySelector('.control-close');
|
|
closeBtn.addEventListener('click', () => {{
|
|
this.collapse();
|
|
}});
|
|
|
|
// Responsive behavior
|
|
window.addEventListener('resize', () => {{
|
|
if (window.innerWidth <= 768) {{
|
|
this.element.style.display = 'none';
|
|
}} else {{
|
|
this.element.style.display = '';
|
|
}}
|
|
}});
|
|
|
|
document.body.appendChild(this.element);
|
|
|
|
// Hide on mobile
|
|
if (window.innerWidth <= 768) {{
|
|
this.element.style.display = 'none';
|
|
}}
|
|
|
|
console.log(`🎛️ ${{this.config.title}} control created`);
|
|
}},
|
|
|
|
styleHeader: function() {{
|
|
const header = this.element.querySelector('.control-header');
|
|
|
|
// Style the header to show icon, title, and close button in one line
|
|
// Match the height of the collapsed icon state (40px)
|
|
header.style.cssText = `
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 40px;
|
|
padding: 0 1rem;
|
|
border-bottom: 1px solid #eee;
|
|
margin-bottom: 0;
|
|
`;
|
|
|
|
const icon = header.querySelector('.control-icon');
|
|
if (icon) {{
|
|
icon.style.cssText = `
|
|
font-size: 16px;
|
|
color: #666;
|
|
margin-right: 0.5rem;
|
|
cursor: grab;
|
|
user-select: none;
|
|
`;
|
|
|
|
// Make icon draggable
|
|
this.setupDragHandlers(icon);
|
|
}}
|
|
|
|
const title = header.querySelector('.control-title');
|
|
if (title) {{
|
|
title.style.cssText = `
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
flex-grow: 1;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
`;
|
|
|
|
// Add click handler to toggle header-only mode
|
|
title.addEventListener('click', () => {{
|
|
this.toggleHeaderOnly();
|
|
}});
|
|
}}
|
|
|
|
const closeBtn = header.querySelector('.control-close');
|
|
if (closeBtn) {{
|
|
closeBtn.style.cssText = `
|
|
background: none;
|
|
border: none;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
color: #6c757d;
|
|
padding: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: color 0.2s ease;
|
|
`;
|
|
}}
|
|
}},
|
|
|
|
styleContent: function() {{
|
|
const content = this.element.querySelector('.control-content');
|
|
const expansion = this.getExpansionDirection();
|
|
|
|
// Style the content area based on expansion direction
|
|
let contentStyles = `
|
|
padding: 0.5rem;
|
|
overflow-y: auto;
|
|
`;
|
|
|
|
if (expansion.body === 'up') {{
|
|
// Body expands upward (for bottom border positions)
|
|
contentStyles += `
|
|
max-height: calc(80vh - 40px);
|
|
`;
|
|
content.parentElement.style.flexDirection = 'column-reverse';
|
|
}} else {{
|
|
// Body expands downward (default)
|
|
contentStyles += `
|
|
max-height: calc(80vh - 40px);
|
|
`;
|
|
content.parentElement.style.flexDirection = 'column';
|
|
}}
|
|
|
|
content.style.cssText = contentStyles;
|
|
}},
|
|
|
|
expand: function() {{
|
|
this.isExpanded = true;
|
|
const panel = this.element.querySelector('.control-panel');
|
|
const toggleBtn = this.element.querySelector('.control-toggle');
|
|
|
|
// Get expansion direction based on compass position
|
|
const expansion = this.getExpansionDirection();
|
|
|
|
// Apply expansion styling based on direction
|
|
if (expansion.header === 'left') {{
|
|
// Header expands to the left (for right border positions)
|
|
this.element.style.width = '280px';
|
|
this.element.style.transformOrigin = 'top right';
|
|
}} else {{
|
|
// Header expands to the right (default)
|
|
this.element.style.width = '280px';
|
|
this.element.style.transformOrigin = 'top left';
|
|
}}
|
|
|
|
panel.style.display = 'block';
|
|
toggleBtn.style.display = 'none';
|
|
|
|
this.styleHeader();
|
|
this.styleContent();
|
|
this.addResizeHandle();
|
|
}},
|
|
|
|
collapse: function() {{
|
|
this.isExpanded = false;
|
|
this.isHeaderOnly = false; // Reset header-only state
|
|
const panel = this.element.querySelector('.control-panel');
|
|
const toggleBtn = this.element.querySelector('.control-toggle');
|
|
panel.style.display = 'none';
|
|
|
|
// Reset size to default
|
|
this.element.style.width = '40px';
|
|
this.element.style.height = 'auto';
|
|
|
|
// Remove resize handle
|
|
this.removeResizeHandle();
|
|
|
|
toggleBtn.style.display = 'block';
|
|
|
|
// Reset position to original compass location
|
|
this.element.style.top = this.originalPosition.top;
|
|
this.element.style.right = this.originalPosition.right;
|
|
this.element.style.bottom = this.originalPosition.bottom;
|
|
this.element.style.left = this.originalPosition.left;
|
|
this.element.style.transform = this.originalPosition.transform;
|
|
}},
|
|
|
|
toggleHeaderOnly: function() {{
|
|
if (!this.isExpanded) {{
|
|
// If collapsed, first expand normally
|
|
this.buildContent();
|
|
return;
|
|
}}
|
|
|
|
const content = this.element.querySelector('.control-content');
|
|
|
|
if (this.isHeaderOnly) {{
|
|
// Show content area (go to full expanded mode)
|
|
this.isHeaderOnly = false;
|
|
content.style.display = 'block';
|
|
console.log(`🎛️ ${{this.config.title}} expanded to full view`);
|
|
}} else {{
|
|
// Hide content area (go to header-only mode)
|
|
this.isHeaderOnly = true;
|
|
content.style.display = 'none';
|
|
console.log(`🎛️ ${{this.config.title}} collapsed to header only`);
|
|
}}
|
|
}},
|
|
|
|
setupDragHandlers: function(dragElement) {{
|
|
dragElement.addEventListener('mousedown', (e) => {{
|
|
this.isDragging = true;
|
|
const rect = this.element.getBoundingClientRect();
|
|
const iconRect = dragElement.getBoundingClientRect();
|
|
|
|
// Calculate offset relative to the icon position, not the element
|
|
this.dragOffset.x = e.clientX - rect.left;
|
|
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
|
|
|
|
dragElement.style.cursor = 'grabbing';
|
|
|
|
e.preventDefault();
|
|
}});
|
|
|
|
document.addEventListener('mousemove', (e) => {{
|
|
if (!this.isDragging || !this.isExpanded) return;
|
|
|
|
const newX = e.clientX - this.dragOffset.x;
|
|
const newY = e.clientY - this.dragOffset.y;
|
|
|
|
// Keep within viewport bounds
|
|
const maxX = window.innerWidth - this.element.offsetWidth;
|
|
const maxY = window.innerHeight - this.element.offsetHeight;
|
|
|
|
const boundedX = Math.max(0, Math.min(newX, maxX));
|
|
const boundedY = Math.max(0, Math.min(newY, maxY));
|
|
|
|
this.element.style.left = boundedX + 'px';
|
|
this.element.style.top = boundedY + 'px';
|
|
}});
|
|
|
|
document.addEventListener('mouseup', () => {{
|
|
if (this.isDragging) {{
|
|
this.isDragging = false;
|
|
dragElement.style.cursor = 'grab';
|
|
}}
|
|
}});
|
|
}},
|
|
|
|
// Add resize handle to expanded control
|
|
addResizeHandle: function() {{
|
|
// Remove existing resize handle if any
|
|
this.removeResizeHandle();
|
|
|
|
const resizeHandle = document.createElement('div');
|
|
resizeHandle.className = 'control-resize-handle';
|
|
// Create small circle for resize handle
|
|
resizeHandle.innerHTML = '';
|
|
resizeHandle.style.cssText = `
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 2px;
|
|
width: 8px;
|
|
height: 8px;
|
|
cursor: nw-resize;
|
|
display: none;
|
|
user-select: none;
|
|
z-index: 1001;
|
|
background: #6c757d;
|
|
border-radius: 50%;
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s ease;
|
|
`;
|
|
|
|
this.element.appendChild(resizeHandle);
|
|
this.setupResizeHandlers(resizeHandle);
|
|
this.setupHoverBehavior();
|
|
}},
|
|
|
|
// Setup hover behavior for resize handle and close button
|
|
setupHoverBehavior: function() {{
|
|
const resizeHandle = this.element.querySelector('.control-resize-handle');
|
|
const closeBtn = this.element.querySelector('.control-close');
|
|
|
|
if (resizeHandle && closeBtn) {{
|
|
// Show/hide on control hover
|
|
this.element.addEventListener('mouseenter', () => {{
|
|
resizeHandle.style.display = 'flex';
|
|
closeBtn.style.display = 'block';
|
|
}});
|
|
|
|
this.element.addEventListener('mouseleave', () => {{
|
|
resizeHandle.style.display = 'none';
|
|
closeBtn.style.display = 'none';
|
|
}});
|
|
}}
|
|
}},
|
|
|
|
// Remove resize handle
|
|
removeResizeHandle: function() {{
|
|
const existingHandle = this.element.querySelector('.control-resize-handle');
|
|
if (existingHandle) {{
|
|
existingHandle.remove();
|
|
}}
|
|
}},
|
|
|
|
// Set up resize event handlers
|
|
setupResizeHandlers: function(resizeHandle) {{
|
|
resizeHandle.addEventListener('mousedown', (e) => {{
|
|
this.isResizing = true;
|
|
const rect = this.element.getBoundingClientRect();
|
|
this.resizeStartSize = {{
|
|
width: rect.width,
|
|
height: rect.height,
|
|
startX: e.clientX,
|
|
startY: e.clientY
|
|
}};
|
|
|
|
resizeHandle.style.cursor = 'nw-resize';
|
|
resizeHandle.style.color = '#28a745';
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation(); // Prevent triggering drag
|
|
}});
|
|
|
|
document.addEventListener('mousemove', (e) => {{
|
|
if (!this.isResizing || !this.isExpanded) return;
|
|
|
|
const deltaX = e.clientX - this.resizeStartSize.startX;
|
|
const deltaY = e.clientY - this.resizeStartSize.startY;
|
|
|
|
const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX);
|
|
const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY);
|
|
|
|
// Check viewport bounds
|
|
const maxWidth = window.innerWidth - this.element.offsetLeft;
|
|
const maxHeight = window.innerHeight - this.element.offsetTop;
|
|
|
|
const boundedWidth = Math.min(newWidth, maxWidth - 20);
|
|
const boundedHeight = Math.min(newHeight, maxHeight - 20);
|
|
|
|
this.element.style.width = boundedWidth + 'px';
|
|
this.element.style.height = boundedHeight + 'px';
|
|
|
|
// Ensure content areas resize properly
|
|
this.updateContentSize();
|
|
}});
|
|
|
|
document.addEventListener('mouseup', () => {{
|
|
if (this.isResizing) {{
|
|
this.isResizing = false;
|
|
resizeHandle.style.cursor = 'nw-resize';
|
|
resizeHandle.style.color = '#6c757d';
|
|
}}
|
|
}});
|
|
}},
|
|
|
|
// Update content area sizes during resize
|
|
updateContentSize: function() {{
|
|
const content = this.element.querySelector('.control-content');
|
|
if (content) {{
|
|
// Adjust content height to fit the resized control
|
|
const headerHeight = 40; // Header is 40px
|
|
const padding = 16; // Account for padding
|
|
const controlHeight = this.element.offsetHeight;
|
|
const availableHeight = controlHeight - headerHeight - padding;
|
|
|
|
content.style.maxHeight = Math.max(100, availableHeight) + 'px';
|
|
}}
|
|
}}
|
|
}};
|
|
|
|
// Step 5: Initialize ContentsControl (new implementation based on Control class)
|
|
try {{
|
|
const contentsControl = Object.create(Control);
|
|
|
|
// Configure for contents navigation
|
|
contentsControl.config = {{
|
|
icon: '☰',
|
|
title: 'Contents',
|
|
className: 'contents-control',
|
|
defaultContent: 'No headings found',
|
|
ariaLabel: 'Document Navigation',
|
|
position: 'wnw' // West-north-west positioning
|
|
}};
|
|
|
|
// Override buildContent method for navigation functionality
|
|
contentsControl.buildContent = function() {{
|
|
const content = this.element.querySelector('.control-content');
|
|
|
|
// Build navigation content from current DOM
|
|
const allHeadings = document.querySelectorAll('h1, h2, h3');
|
|
// Filter out headings that contain "Contents" or similar navigation-related text
|
|
const headings = Array.from(allHeadings).filter(heading => {{
|
|
const text = heading.textContent.trim().toLowerCase();
|
|
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
|
|
}});
|
|
console.log("📋 Found headings for navigation:", headings.length);
|
|
|
|
if (headings.length === 0) {{
|
|
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
|
|
}} else {{
|
|
let navHtml = '';
|
|
headings.forEach((heading, index) => {{
|
|
if (!heading.id) {{
|
|
heading.id = `heading-${{index + 1}}`;
|
|
}}
|
|
const level = parseInt(heading.tagName.substring(1));
|
|
const indent = (level - 1) * 1;
|
|
navHtml += `
|
|
<a href="#${{heading.id}}"
|
|
style="display: block; padding: 0.5rem; margin-left: ${{indent}}rem;
|
|
text-decoration: none; color: #333; font-size: 0.9rem;
|
|
border-radius: 4px; cursor: pointer;"
|
|
onmouseover="this.style.backgroundColor='#f5f5f5'"
|
|
onmouseout="this.style.backgroundColor=''"
|
|
onclick="event.preventDefault(); document.getElementById('${{heading.id}}').scrollIntoView({{behavior: 'smooth'}}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
|
|
${{heading.textContent.trim()}}
|
|
</a>
|
|
`;
|
|
}});
|
|
content.innerHTML = navHtml;
|
|
}}
|
|
|
|
// Show panel
|
|
this.expand();
|
|
}};
|
|
|
|
// Initialize the ContentsControl
|
|
contentsControl.createControl();
|
|
|
|
// Make globally available for mobile collapse
|
|
window.contentsControl = contentsControl;
|
|
}} catch (error) {{
|
|
console.error("ContentsControl failed to initialize:", error);
|
|
}}
|
|
|
|
}});
|
|
|
|
// Handle CDN loading errors
|
|
window.addEventListener('load', function() {{
|
|
if (window.markitectMarkedError) {{
|
|
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
|
}}
|
|
}});
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html_template
|
|
|
|
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/section-manager.js',
|
|
'js/components/debug-panel.js',
|
|
'js/components/document-controls.js',
|
|
'js/components/dom-renderer.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 = """
|
|
// === 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();
|
|
|
|
// Define abstract Control class for UI controls (same as viewing mode)
|
|
const Control = {
|
|
// Abstract control properties
|
|
element: null,
|
|
isExpanded: false,
|
|
isHeaderOnly: false, // New state for header-only mode
|
|
isDragging: false,
|
|
isResizing: false, // New state for resizing mode
|
|
dragOffset: { x: 0, y: 0 },
|
|
resizeStartSize: { width: 280, height: 'auto' },
|
|
originalPosition: { top: '80px', left: '20px' },
|
|
defaultSize: { width: 280, minWidth: 200, minHeight: 150 },
|
|
|
|
// Configuration properties (to be overridden by subclasses)
|
|
config: {
|
|
icon: '?',
|
|
title: 'Control',
|
|
className: 'control',
|
|
defaultContent: 'Template only',
|
|
ariaLabel: 'Control',
|
|
position: 'w' // Default compass position: west (middle-left)
|
|
},
|
|
|
|
// Compass positioning system (top-aligned for proper expansion)
|
|
compassPositions: {
|
|
// North positions (top)
|
|
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
|
|
'nne': { top: '20px', left: '65%', transform: 'translateX(-50%)' },
|
|
'ne': { top: '20px', right: '20px' },
|
|
'ene': { top: '80px', right: '20px' }, // Top-aligned instead of center
|
|
|
|
// East positions (right)
|
|
'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
|
|
'ese': { top: 'calc(65vh - 20px)', right: '20px' }, // Top-aligned
|
|
'se': { bottom: '20px', right: '20px' },
|
|
'sse': { bottom: '20px', right: '35%', transform: 'translateX(50%)' },
|
|
|
|
// South positions (bottom)
|
|
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
|
|
'ssw': { bottom: '20px', left: '35%', transform: 'translateX(-50%)' },
|
|
'sw': { bottom: '20px', left: '20px' },
|
|
'wsw': { bottom: '80px', left: '20px' }, // Top-aligned instead of center
|
|
|
|
// West positions (left) - top-aligned for proper expansion
|
|
'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
|
|
'wnw': { top: '80px', left: '20px' }, // Top-aligned instead of center
|
|
'nw': { top: '20px', left: '20px' },
|
|
'nnw': { top: '20px', left: '35%', transform: 'translateX(-50%)' }
|
|
},
|
|
|
|
// Get expansion direction based on compass position
|
|
getExpansionDirection: function() {
|
|
const pos = this.config.position;
|
|
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
|
|
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
|
|
|
|
return {
|
|
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
|
|
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
|
|
};
|
|
},
|
|
|
|
// Calculate position styles based on compass direction
|
|
getPositionStyles: function() {
|
|
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
|
|
return {
|
|
position: 'fixed',
|
|
top: compassPos.top || 'auto',
|
|
right: compassPos.right || 'auto',
|
|
bottom: compassPos.bottom || 'auto',
|
|
left: compassPos.left || 'auto',
|
|
transform: compassPos.transform || 'none',
|
|
zIndex: 1001
|
|
};
|
|
},
|
|
|
|
// Abstract methods (to be implemented by subclasses)
|
|
buildContent: function() {
|
|
const content = this.element.querySelector('.control-content');
|
|
content.innerHTML = `<p style="padding: 1rem; color: #666;">${this.config.defaultContent}</p>`;
|
|
},
|
|
|
|
// Concrete methods (shared by all controls)
|
|
createControl: function() {
|
|
console.log(`🎛️ Creating ${this.config.title} control...`);
|
|
|
|
this.element = document.createElement('div');
|
|
this.element.className = this.config.className;
|
|
this.element.innerHTML = `
|
|
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
|
|
<div class="control-panel" style="display: none;">
|
|
<div class="control-header">
|
|
<span class="control-icon">${this.config.icon}</span>
|
|
<span class="control-title">${this.config.title}</span>
|
|
<button class="control-close">✕</button>
|
|
</div>
|
|
<div class="control-content">Loading...</div>
|
|
</div>
|
|
`;
|
|
|
|
// Position using compass direction
|
|
const positionStyles = this.getPositionStyles();
|
|
this.element.style.cssText = `
|
|
position: ${positionStyles.position};
|
|
top: ${positionStyles.top};
|
|
right: ${positionStyles.right};
|
|
bottom: ${positionStyles.bottom};
|
|
left: ${positionStyles.left};
|
|
transform: ${positionStyles.transform};
|
|
z-index: ${positionStyles.zIndex};
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border: 1px solid #e1e5e9;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
backdrop-filter: blur(8px);
|
|
width: 40px;
|
|
transition: all 0.3s ease;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
`;
|
|
|
|
// Store original position for reset
|
|
this.originalPosition = {
|
|
top: positionStyles.top,
|
|
right: positionStyles.right,
|
|
bottom: positionStyles.bottom,
|
|
left: positionStyles.left,
|
|
transform: positionStyles.transform
|
|
};
|
|
|
|
// Style toggle button
|
|
const toggleBtn = this.element.querySelector('.control-toggle');
|
|
toggleBtn.style.cssText = `
|
|
width: 100%;
|
|
height: 40px;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
color: #666;
|
|
transition: color 0.2s ease;
|
|
`;
|
|
|
|
// Handle click to build content on-demand
|
|
toggleBtn.addEventListener('click', () => {
|
|
if (this.isExpanded) {
|
|
this.collapse();
|
|
} else {
|
|
console.log(`🎛️ ${this.config.title} toggle clicked - building content...`);
|
|
this.buildContent();
|
|
}
|
|
});
|
|
|
|
// Close button handler
|
|
const closeBtn = this.element.querySelector('.control-close');
|
|
closeBtn.addEventListener('click', () => {
|
|
this.collapse();
|
|
});
|
|
|
|
document.body.appendChild(this.element);
|
|
console.log(`🎛️ ${this.config.title} control created`);
|
|
},
|
|
|
|
styleHeader: function() {
|
|
const header = this.element.querySelector('.control-header');
|
|
|
|
// Style the header to show icon, title, and close button in one line
|
|
// Match the height of the collapsed icon state (40px)
|
|
header.style.cssText = `
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 40px;
|
|
padding: 0 1rem;
|
|
border-bottom: 1px solid #eee;
|
|
margin-bottom: 0;
|
|
`;
|
|
|
|
const icon = header.querySelector('.control-icon');
|
|
if (icon) {
|
|
icon.style.cssText = `
|
|
font-size: 16px;
|
|
color: #666;
|
|
margin-right: 0.5rem;
|
|
cursor: grab;
|
|
user-select: none;
|
|
`;
|
|
|
|
// Make icon draggable
|
|
this.setupDragHandlers(icon);
|
|
}
|
|
|
|
const title = header.querySelector('.control-title');
|
|
if (title) {
|
|
title.style.cssText = `
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
flex-grow: 1;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
`;
|
|
|
|
// Add click handler to toggle header-only mode
|
|
title.addEventListener('click', () => {
|
|
this.toggleHeaderOnly();
|
|
});
|
|
}
|
|
|
|
const closeBtn = header.querySelector('.control-close');
|
|
if (closeBtn) {
|
|
closeBtn.style.cssText = `
|
|
background: none;
|
|
border: none;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
color: #6c757d;
|
|
padding: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: color 0.2s ease;
|
|
`;
|
|
}
|
|
},
|
|
|
|
styleContent: function() {
|
|
const content = this.element.querySelector('.control-content');
|
|
const expansion = this.getExpansionDirection();
|
|
|
|
// Style the content area based on expansion direction
|
|
let contentStyles = `
|
|
padding: 0.5rem;
|
|
overflow-y: auto;
|
|
`;
|
|
|
|
if (expansion.body === 'up') {
|
|
// Body expands upward (for bottom border positions)
|
|
contentStyles += `
|
|
max-height: calc(80vh - 40px);
|
|
`;
|
|
content.parentElement.style.flexDirection = 'column-reverse';
|
|
} else {
|
|
// Body expands downward (default)
|
|
contentStyles += `
|
|
max-height: calc(80vh - 40px);
|
|
`;
|
|
content.parentElement.style.flexDirection = 'column';
|
|
}
|
|
|
|
content.style.cssText = contentStyles;
|
|
},
|
|
|
|
expand: function() {
|
|
this.isExpanded = true;
|
|
const panel = this.element.querySelector('.control-panel');
|
|
const toggleBtn = this.element.querySelector('.control-toggle');
|
|
|
|
// Get expansion direction based on compass position
|
|
const expansion = this.getExpansionDirection();
|
|
|
|
// Apply expansion styling based on direction
|
|
if (expansion.header === 'left') {
|
|
// Header expands to the left (for right border positions)
|
|
this.element.style.width = '300px';
|
|
this.element.style.transformOrigin = 'top right';
|
|
} else {
|
|
// Header expands to the right (default)
|
|
this.element.style.width = '300px';
|
|
this.element.style.transformOrigin = 'top left';
|
|
}
|
|
|
|
panel.style.display = 'block';
|
|
toggleBtn.style.display = 'none';
|
|
|
|
this.styleHeader();
|
|
this.styleContent();
|
|
this.addResizeHandle();
|
|
},
|
|
|
|
collapse: function() {
|
|
this.isExpanded = false;
|
|
this.isHeaderOnly = false; // Reset header-only state
|
|
const panel = this.element.querySelector('.control-panel');
|
|
const toggleBtn = this.element.querySelector('.control-toggle');
|
|
panel.style.display = 'none';
|
|
|
|
// Reset size to default
|
|
this.element.style.width = '40px';
|
|
this.element.style.height = 'auto';
|
|
|
|
// Remove resize handle
|
|
this.removeResizeHandle();
|
|
|
|
toggleBtn.style.display = 'block';
|
|
|
|
// Reset position to original compass location
|
|
this.element.style.top = this.originalPosition.top;
|
|
this.element.style.right = this.originalPosition.right;
|
|
this.element.style.bottom = this.originalPosition.bottom;
|
|
this.element.style.left = this.originalPosition.left;
|
|
this.element.style.transform = this.originalPosition.transform;
|
|
},
|
|
|
|
toggleHeaderOnly: function() {
|
|
if (!this.isExpanded) {
|
|
// If collapsed, first expand normally
|
|
this.buildContent();
|
|
return;
|
|
}
|
|
|
|
const content = this.element.querySelector('.control-content');
|
|
|
|
if (this.isHeaderOnly) {
|
|
// Show content area (go to full expanded mode)
|
|
this.isHeaderOnly = false;
|
|
content.style.display = 'block';
|
|
console.log(`🎛️ ${this.config.title} expanded to full view`);
|
|
} else {
|
|
// Hide content area (go to header-only mode)
|
|
this.isHeaderOnly = true;
|
|
content.style.display = 'none';
|
|
console.log(`🎛️ ${this.config.title} collapsed to header only`);
|
|
}
|
|
},
|
|
|
|
setupDragHandlers: function(dragElement) {
|
|
dragElement.addEventListener('mousedown', (e) => {
|
|
this.isDragging = true;
|
|
const rect = this.element.getBoundingClientRect();
|
|
const iconRect = dragElement.getBoundingClientRect();
|
|
|
|
// Calculate offset relative to the icon position, not the element
|
|
this.dragOffset.x = e.clientX - rect.left;
|
|
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
|
|
|
|
dragElement.style.cursor = 'grabbing';
|
|
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!this.isDragging || !this.isExpanded) return;
|
|
|
|
const newX = e.clientX - this.dragOffset.x;
|
|
const newY = e.clientY - this.dragOffset.y;
|
|
|
|
// Keep within viewport bounds
|
|
const maxX = window.innerWidth - this.element.offsetWidth;
|
|
const maxY = window.innerHeight - this.element.offsetHeight;
|
|
|
|
const boundedX = Math.max(0, Math.min(newX, maxX));
|
|
const boundedY = Math.max(0, Math.min(newY, maxY));
|
|
|
|
this.element.style.left = boundedX + 'px';
|
|
this.element.style.top = boundedY + 'px';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (this.isDragging) {
|
|
this.isDragging = false;
|
|
dragElement.style.cursor = 'grab';
|
|
}
|
|
});
|
|
},
|
|
|
|
// Add resize handle to expanded control (same as viewing mode)
|
|
addResizeHandle: function() {
|
|
// Remove existing resize handle if any
|
|
this.removeResizeHandle();
|
|
|
|
const resizeHandle = document.createElement('div');
|
|
resizeHandle.className = 'control-resize-handle';
|
|
// Create small circle for resize handle
|
|
resizeHandle.innerHTML = '';
|
|
resizeHandle.style.cssText = `
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 2px;
|
|
width: 8px;
|
|
height: 8px;
|
|
cursor: nw-resize;
|
|
display: none;
|
|
user-select: none;
|
|
z-index: 1001;
|
|
background: #6c757d;
|
|
border-radius: 50%;
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s ease;
|
|
`;
|
|
|
|
this.element.appendChild(resizeHandle);
|
|
this.setupResizeHandlers(resizeHandle);
|
|
this.setupHoverBehavior();
|
|
},
|
|
|
|
// Remove resize handle
|
|
removeResizeHandle: function() {
|
|
const existingHandle = this.element.querySelector('.control-resize-handle');
|
|
if (existingHandle) {
|
|
existingHandle.remove();
|
|
}
|
|
},
|
|
|
|
// Set up resize event handlers
|
|
setupResizeHandlers: function(resizeHandle) {
|
|
resizeHandle.addEventListener('mousedown', (e) => {
|
|
this.isResizing = true;
|
|
const rect = this.element.getBoundingClientRect();
|
|
this.resizeStartSize = {
|
|
width: rect.width,
|
|
height: rect.height,
|
|
startX: e.clientX,
|
|
startY: e.clientY
|
|
};
|
|
|
|
resizeHandle.style.cursor = 'nw-resize';
|
|
resizeHandle.style.background = 'rgba(40, 167, 69, 0.9)';
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation(); // Prevent triggering drag
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!this.isResizing || !this.isExpanded) return;
|
|
|
|
const deltaX = e.clientX - this.resizeStartSize.startX;
|
|
const deltaY = e.clientY - this.resizeStartSize.startY;
|
|
|
|
const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX);
|
|
const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY);
|
|
|
|
// Check viewport bounds
|
|
const maxWidth = window.innerWidth - this.element.offsetLeft;
|
|
const maxHeight = window.innerHeight - this.element.offsetTop;
|
|
|
|
const boundedWidth = Math.min(newWidth, maxWidth - 20);
|
|
const boundedHeight = Math.min(newHeight, maxHeight - 20);
|
|
|
|
this.element.style.width = boundedWidth + 'px';
|
|
this.element.style.height = boundedHeight + 'px';
|
|
|
|
// Ensure content areas resize properly
|
|
this.updateContentSize();
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (this.isResizing) {
|
|
this.isResizing = false;
|
|
resizeHandle.style.cursor = 'nw-resize';
|
|
resizeHandle.style.color = '#6c757d';
|
|
}
|
|
});
|
|
},
|
|
|
|
// Update content area sizes during resize
|
|
updateContentSize: function() {
|
|
const content = this.element.querySelector('.control-content');
|
|
if (content) {
|
|
// Adjust content height to fit the resized control
|
|
const headerHeight = 40; // Header is 40px
|
|
const padding = 16; // Account for padding
|
|
const controlHeight = this.element.offsetHeight;
|
|
const availableHeight = controlHeight - headerHeight - padding;
|
|
|
|
content.style.maxHeight = Math.max(100, availableHeight) + 'px';
|
|
}
|
|
},
|
|
|
|
// Setup hover behavior for resize handle and close button
|
|
setupHoverBehavior: function() {
|
|
const resizeHandle = this.element.querySelector('.control-resize-handle');
|
|
const closeBtn = this.element.querySelector('.control-close');
|
|
|
|
if (resizeHandle && closeBtn) {
|
|
// Show/hide on control hover
|
|
this.element.addEventListener('mouseenter', () => {
|
|
resizeHandle.style.display = 'flex';
|
|
closeBtn.style.display = 'block';
|
|
});
|
|
|
|
this.element.addEventListener('mouseleave', () => {
|
|
resizeHandle.style.display = 'none';
|
|
closeBtn.style.display = 'none';
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// Create ContentsControl for edit mode (new implementation based on Control class)
|
|
try {
|
|
const contentsControl = Object.create(Control);
|
|
|
|
// Configure for contents navigation in edit mode
|
|
contentsControl.config = {
|
|
icon: '☰',
|
|
title: 'Contents',
|
|
className: 'contents-control edit-mode',
|
|
defaultContent: 'No headings found',
|
|
ariaLabel: 'Document Navigation',
|
|
position: 'wnw' // West-north-west positioning
|
|
};
|
|
|
|
// Override buildContent method for navigation functionality
|
|
contentsControl.buildContent = function() {
|
|
const content = this.element.querySelector('.control-content');
|
|
|
|
// Build navigation content from current DOM
|
|
const allHeadings = document.querySelectorAll('h1, h2, h3');
|
|
// Filter out headings that contain "Contents" or similar navigation-related text
|
|
const headings = Array.from(allHeadings).filter(heading => {
|
|
const text = heading.textContent.trim().toLowerCase();
|
|
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
|
|
});
|
|
console.log("📋 Found headings for navigation:", headings.length);
|
|
|
|
if (headings.length === 0) {
|
|
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
|
|
} else {
|
|
let navHtml = '';
|
|
headings.forEach((heading, index) => {
|
|
if (!heading.id) {
|
|
heading.id = `heading-${index + 1}`;
|
|
}
|
|
const level = parseInt(heading.tagName.substring(1));
|
|
const indent = (level - 1) * 1;
|
|
navHtml += `
|
|
<a href="#${heading.id}"
|
|
style="display: block; padding: 0.5rem; margin-left: ${indent}rem;
|
|
text-decoration: none; color: #333; font-size: 0.9rem;
|
|
border-radius: 4px; cursor: pointer;"
|
|
onmouseover="this.style.backgroundColor='#f5f5f5'"
|
|
onmouseout="this.style.backgroundColor=''"
|
|
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
|
|
${heading.textContent.trim()}
|
|
</a>
|
|
`;
|
|
});
|
|
content.innerHTML = navHtml;
|
|
}
|
|
|
|
// Show panel
|
|
this.expand();
|
|
};
|
|
|
|
// Initialize the ContentsControl
|
|
contentsControl.createControl();
|
|
|
|
// Make globally available for mobile collapse
|
|
window.contentsControl = contentsControl;
|
|
} catch (error) {
|
|
console.error("ContentsControl failed to initialize:", error);
|
|
}
|
|
|
|
// Step 7: Initialize Independent Debug System
|
|
try {
|
|
// Create independent debug system using IndexedDB for persistence
|
|
window.MarkitectDebugSystem = {
|
|
db: null,
|
|
messages: [],
|
|
maxMessages: 1000,
|
|
isEnabled: true,
|
|
subscribers: [],
|
|
|
|
// Selection and filtering system
|
|
selectionCriteria: {
|
|
includeDocumentEvents: true,
|
|
includeSystemEvents: false,
|
|
includeControlEvents: true,
|
|
includeEditingEvents: true,
|
|
includeNavigationEvents: false,
|
|
includedHeadings: new Set(), // Track which document headings to monitor
|
|
excludedSources: new Set(['ContentsControl', 'DocumentNavigator'])
|
|
},
|
|
|
|
// Initialize IndexedDB for persistence
|
|
async init() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('MarkitectDebugDB', 1);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => {
|
|
this.db = request.result;
|
|
this.loadMessages().then(resolve);
|
|
};
|
|
|
|
request.onupgradeneeded = (e) => {
|
|
const db = e.target.result;
|
|
if (!db.objectStoreNames.contains('messages')) {
|
|
const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
|
|
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
store.createIndex('category', 'category', { unique: false });
|
|
}
|
|
};
|
|
});
|
|
},
|
|
|
|
// Add a debug message with selection filtering
|
|
async addMessage(message, category = 'INFO', source = 'System', context = {}) {
|
|
// Check if this message should be included based on selection criteria
|
|
if (!this.shouldIncludeMessage(message, category, source, context)) {
|
|
return null;
|
|
}
|
|
|
|
const messageObj = {
|
|
message: String(message),
|
|
category: category.toUpperCase(),
|
|
source: source,
|
|
context: context,
|
|
timestamp: new Date().toISOString(),
|
|
displayTime: new Date().toLocaleTimeString()
|
|
};
|
|
|
|
// Add to memory
|
|
this.messages.push(messageObj);
|
|
|
|
// Keep only last maxMessages in memory
|
|
if (this.messages.length > this.maxMessages) {
|
|
this.messages = this.messages.slice(-this.maxMessages);
|
|
}
|
|
|
|
// Persist to IndexedDB
|
|
if (this.db) {
|
|
try {
|
|
const transaction = this.db.transaction(['messages'], 'readwrite');
|
|
const store = transaction.objectStore('messages');
|
|
await store.add(messageObj);
|
|
} catch (e) {
|
|
console.warn('Failed to persist debug message:', e);
|
|
}
|
|
}
|
|
|
|
// Notify subscribers
|
|
this.subscribers.forEach(callback => {
|
|
try { callback(messageObj); } catch (e) { console.error('Debug subscriber error:', e); }
|
|
});
|
|
|
|
return messageObj;
|
|
},
|
|
|
|
// Selection filtering logic
|
|
shouldIncludeMessage(message, category, source, context) {
|
|
if (!this.isEnabled) return false;
|
|
|
|
// Check excluded sources
|
|
if (this.selectionCriteria.excludedSources.has(source)) {
|
|
return false;
|
|
}
|
|
|
|
// Category-based filtering
|
|
const categoryChecks = {
|
|
'DOCUMENT': () => this.selectionCriteria.includeDocumentEvents,
|
|
'SYSTEM': () => this.selectionCriteria.includeSystemEvents,
|
|
'CONTROL': () => this.selectionCriteria.includeControlEvents,
|
|
'EDITING': () => this.selectionCriteria.includeEditingEvents,
|
|
'NAVIGATION': () => this.selectionCriteria.includeNavigationEvents
|
|
};
|
|
|
|
// Check if we have a specific category filter
|
|
if (context.eventType && categoryChecks[context.eventType]) {
|
|
return categoryChecks[context.eventType]();
|
|
}
|
|
|
|
// Document heading specific filtering
|
|
if (context.headingId) {
|
|
return this.selectionCriteria.includedHeadings.has(context.headingId) ||
|
|
this.selectionCriteria.includedHeadings.size === 0; // Include all if none specifically selected
|
|
}
|
|
|
|
// Default: include based on general settings
|
|
return category === 'ERROR' || // Always include errors
|
|
this.selectionCriteria.includeSystemEvents;
|
|
},
|
|
|
|
// Document structure awareness
|
|
scanDocumentStructure() {
|
|
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
const documentStructure = {
|
|
headings: [],
|
|
totalSections: 0
|
|
};
|
|
|
|
headings.forEach((heading, index) => {
|
|
// Skip control headings (like Contents, Debug, etc.)
|
|
const text = heading.textContent.trim().toLowerCase();
|
|
if (text.includes('contents') || text.includes('debug') ||
|
|
text.includes('control') || text.includes('navigation')) {
|
|
return;
|
|
}
|
|
|
|
if (!heading.id) {
|
|
heading.id = `document-heading-${index + 1}`;
|
|
}
|
|
|
|
documentStructure.headings.push({
|
|
id: heading.id,
|
|
text: heading.textContent.trim(),
|
|
level: parseInt(heading.tagName.substring(1)),
|
|
element: heading
|
|
});
|
|
});
|
|
|
|
documentStructure.totalSections = documentStructure.headings.length;
|
|
this.documentStructure = documentStructure;
|
|
|
|
// Auto-include all document headings for monitoring
|
|
this.selectionCriteria.includedHeadings.clear();
|
|
documentStructure.headings.forEach(heading => {
|
|
this.selectionCriteria.includedHeadings.add(heading.id);
|
|
});
|
|
|
|
this.addMessage(`Document structure scanned: ${documentStructure.totalSections} sections found`,
|
|
'INFO', 'DocumentScanner', { eventType: 'DOCUMENT', headings: documentStructure.headings });
|
|
|
|
return documentStructure;
|
|
},
|
|
|
|
// Selection management methods
|
|
includeHeading(headingId) {
|
|
this.selectionCriteria.includedHeadings.add(headingId);
|
|
},
|
|
|
|
excludeHeading(headingId) {
|
|
this.selectionCriteria.includedHeadings.delete(headingId);
|
|
},
|
|
|
|
includeSource(source) {
|
|
this.selectionCriteria.excludedSources.delete(source);
|
|
},
|
|
|
|
excludeSource(source) {
|
|
this.selectionCriteria.excludedSources.add(source);
|
|
},
|
|
|
|
// Get selection criteria for UI
|
|
getSelectionCriteria() {
|
|
return {
|
|
...this.selectionCriteria,
|
|
includedHeadings: Array.from(this.selectionCriteria.includedHeadings),
|
|
excludedSources: Array.from(this.selectionCriteria.excludedSources)
|
|
};
|
|
},
|
|
|
|
// Load messages from IndexedDB
|
|
async loadMessages() {
|
|
if (!this.db) return;
|
|
|
|
try {
|
|
const transaction = this.db.transaction(['messages'], 'readonly');
|
|
const store = transaction.objectStore('messages');
|
|
const request = store.getAll();
|
|
|
|
request.onsuccess = () => {
|
|
this.messages = request.result.slice(-this.maxMessages);
|
|
console.log(`📊 Loaded ${this.messages.length} debug messages from IndexedDB`);
|
|
};
|
|
} catch (e) {
|
|
console.warn('Failed to load debug messages:', e);
|
|
}
|
|
},
|
|
|
|
// Clear all messages
|
|
async clearMessages() {
|
|
this.messages = [];
|
|
|
|
if (this.db) {
|
|
try {
|
|
const transaction = this.db.transaction(['messages'], 'readwrite');
|
|
const store = transaction.objectStore('messages');
|
|
await store.clear();
|
|
} catch (e) {
|
|
console.warn('Failed to clear debug messages from DB:', e);
|
|
}
|
|
}
|
|
|
|
this.subscribers.forEach(callback => {
|
|
try { callback({ type: 'clear' }); } catch (e) { console.error('Debug subscriber error:', e); }
|
|
});
|
|
},
|
|
|
|
// Subscribe to debug updates
|
|
subscribe(callback) {
|
|
this.subscribers.push(callback);
|
|
return () => {
|
|
const index = this.subscribers.indexOf(callback);
|
|
if (index > -1) this.subscribers.splice(index, 1);
|
|
};
|
|
},
|
|
|
|
// Get messages (with filtering)
|
|
getMessages(category = null, limit = null) {
|
|
let filtered = this.messages;
|
|
|
|
if (category) {
|
|
filtered = filtered.filter(msg => msg.category === category.toUpperCase());
|
|
}
|
|
|
|
if (limit) {
|
|
filtered = filtered.slice(-limit);
|
|
}
|
|
|
|
return filtered;
|
|
},
|
|
|
|
// Get recent messages
|
|
getRecentMessages(count = 50) {
|
|
return this.messages.slice(-count);
|
|
},
|
|
|
|
// Export messages as JSON
|
|
exportMessages() {
|
|
return JSON.stringify(this.messages, null, 2);
|
|
}
|
|
};
|
|
|
|
// Initialize the debug system
|
|
window.MarkitectDebugSystem.init().then(() => {
|
|
console.log('📊 Markitect Debug System initialized');
|
|
|
|
// Add initial message
|
|
window.MarkitectDebugSystem.addMessage('Markitect Debug System initialized', 'INFO', 'DebugSystem', {eventType: 'SYSTEM'});
|
|
|
|
}).catch(error => {
|
|
console.warn('📊 Debug System initialization failed, using memory only:', error);
|
|
window.MarkitectDebugSystem.addMessage('Debug System initialized (memory only)', 'WARNING', 'DebugSystem', {eventType: 'SYSTEM'});
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("Debug System initialization failed:", error);
|
|
}
|
|
|
|
// Step 8: Initialize DebugControl (new implementation based on Control class)
|
|
try {
|
|
const debugControl = Object.create(Control);
|
|
|
|
// Configure for debug functionality
|
|
debugControl.config = {
|
|
icon: '🪲',
|
|
title: 'Debug',
|
|
className: 'debug-control',
|
|
defaultContent: 'Debug panel controls',
|
|
ariaLabel: 'Debug Control',
|
|
position: 'ese' // East-south-east positioning
|
|
};
|
|
|
|
// Override buildContent method for debug functionality
|
|
debugControl.buildContent = function() {
|
|
console.log("🪲 Building debug control content...");
|
|
|
|
try {
|
|
const content = this.element.querySelector('.control-content');
|
|
if (!content) {
|
|
console.error("🪲 Debug control content element not found");
|
|
return;
|
|
}
|
|
|
|
// Build debug control panel with selection and filtering
|
|
content.innerHTML = `
|
|
<div style="padding: 0.5rem;">
|
|
<!-- Debug Control Header -->
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;
|
|
padding-bottom: 0.5rem; border-bottom: 1px solid #eee;">
|
|
<h4 style="margin: 0; font-size: 0.9rem; font-weight: 600;">Debug Messages</h4>
|
|
<div>
|
|
<button onclick="window.MarkitectDebugSystem?.scanDocumentStructure()"
|
|
style="padding: 0.25rem 0.5rem; background: #6c757d; color: white;
|
|
border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem; margin-right: 0.25rem;">
|
|
📊 Scan
|
|
</button>
|
|
<button onclick="window.MarkitectDebugSystem?.clearMessages()"
|
|
style="padding: 0.25rem 0.5rem; background: #dc3545; color: white;
|
|
border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selection Controls -->
|
|
<div style="margin-bottom: 0.5rem; padding: 0.5rem; background: #f8f9fa; border-radius: 4px; font-size: 0.8rem;">
|
|
<div style="font-weight: bold; margin-bottom: 0.25rem;">Event Types:</div>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.25rem;">
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
<input type="checkbox" id="docEvents" checked onchange="window.MarkitectDebugSystem.selectionCriteria.includeDocumentEvents = this.checked" style="margin-right: 0.25rem;">
|
|
<span style="font-size: 0.75rem;">Document</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
<input type="checkbox" id="editEvents" checked onchange="window.MarkitectDebugSystem.selectionCriteria.includeEditingEvents = this.checked" style="margin-right: 0.25rem;">
|
|
<span style="font-size: 0.75rem;">Editing</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
<input type="checkbox" id="ctrlEvents" checked onchange="window.MarkitectDebugSystem.selectionCriteria.includeControlEvents = this.checked" style="margin-right: 0.25rem;">
|
|
<span style="font-size: 0.75rem;">Controls</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
<input type="checkbox" id="sysEvents" onchange="window.MarkitectDebugSystem.selectionCriteria.includeSystemEvents = this.checked" style="margin-right: 0.25rem;">
|
|
<span style="font-size: 0.75rem;">System</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Debug Switch -->
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;
|
|
padding: 0.5rem; background: #e9ecef; border-radius: 4px;">
|
|
<span style="font-size: 0.9rem; font-weight: bold;">Debug System:</span>
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
<input type="checkbox" id="debugToggle" checked
|
|
onchange="window.MarkitectDebugSystem.isEnabled = this.checked; document.getElementById('debugStatus').textContent = this.checked ? 'ON' : 'OFF'; document.getElementById('debugStatus').style.color = this.checked ? '#28a745' : '#dc3545';"
|
|
style="margin-right: 0.5rem;">
|
|
<span id="debugStatus" style="color: #28a745; font-weight: bold;">ON</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Debug Messages Container -->
|
|
<div id="debugMessagesContainer" style="max-height: 180px; overflow-y: auto;
|
|
border: 1px solid #ddd; border-radius: 4px;
|
|
background: #f8f9fa; font-family: monospace; font-size: 0.8rem;">
|
|
<div style="padding: 0.5rem; color: #666; text-align: center;">
|
|
Click 📊 Scan to analyze document structure
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Export and Tools -->
|
|
<div style="margin-top: 0.5rem; display: flex; gap: 0.25rem;">
|
|
<button onclick="navigator.clipboard.writeText(window.MarkitectDebugSystem.exportMessages()).then(() => window.MarkitectDebugSystem.addMessage('Debug messages exported to clipboard', 'SUCCESS', 'DebugControl'))"
|
|
style="flex: 1; padding: 0.4rem; background: #17a2b8; color: white;
|
|
border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
📋 Export
|
|
</button>
|
|
<button onclick="console.table(window.MarkitectDebugSystem.getSelectionCriteria())"
|
|
style="flex: 1; padding: 0.4rem; background: #6f42c1; color: white;
|
|
border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
🔍 Criteria
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
console.log("🪲 Debug control HTML content built successfully");
|
|
|
|
// Note: Debug switch state is set directly in HTML (checked attribute)
|
|
// No need to call updateDebugStatus() since checkbox manages its own state
|
|
|
|
// Set up periodic update of debug messages
|
|
try {
|
|
this.setupMessageUpdates();
|
|
} catch (e) {
|
|
console.log("🪲 Message updates setup failed:", e);
|
|
}
|
|
|
|
// Show panel and call expand
|
|
console.log("🪲 Calling expand...");
|
|
this.expand();
|
|
console.log("🪲 Expand called successfully");
|
|
|
|
} catch (error) {
|
|
console.error("🪲 Error in buildContent:", error);
|
|
// Fallback simple content
|
|
const content = this.element.querySelector('.control-content');
|
|
if (content) {
|
|
content.innerHTML = `<div style="padding: 1rem; color: #666;">Debug control error: ${error.message}</div>`;
|
|
this.expand();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add method to update debug switch status (updated for new debug system)
|
|
debugControl.updateDebugStatus = function() {
|
|
const checkbox = this.element.querySelector('#debugToggle');
|
|
const statusText = this.element.querySelector('#debugStatus');
|
|
|
|
if (checkbox && statusText) {
|
|
try {
|
|
if (window.MarkitectDebugSystem) {
|
|
// Use the new debug system's enabled state
|
|
const isEnabled = window.MarkitectDebugSystem.isEnabled;
|
|
checkbox.checked = isEnabled;
|
|
statusText.textContent = isEnabled ? 'ON' : 'OFF';
|
|
statusText.style.color = isEnabled ? '#28a745' : '#dc3545';
|
|
} else {
|
|
// Fallback when debug system is not available
|
|
checkbox.checked = false;
|
|
statusText.textContent = 'UNAVAILABLE';
|
|
statusText.style.color = '#dc3545';
|
|
}
|
|
} catch (e) {
|
|
console.log("🪲 Error updating debug status:", e);
|
|
checkbox.checked = false;
|
|
statusText.textContent = 'ERROR';
|
|
statusText.style.color = '#dc3545';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add method to setup message updates
|
|
debugControl.setupMessageUpdates = function() {
|
|
try {
|
|
// Update messages every 500ms when debug control is open
|
|
this.messageUpdateInterval = setInterval(() => {
|
|
if (this.isExpanded && !this.isHeaderOnly) {
|
|
this.updateMessages();
|
|
// Note: updateDebugStatus() removed - checkbox manages its own state
|
|
}
|
|
}, 500);
|
|
console.log("🪲 Message update interval set up successfully");
|
|
} catch (e) {
|
|
console.log("🪲 Failed to set up message updates:", e);
|
|
}
|
|
};
|
|
|
|
// Add method to update debug messages display using new system
|
|
debugControl.updateMessages = function() {
|
|
const container = this.element.querySelector('#debugMessagesContainer');
|
|
if (!container) return;
|
|
|
|
try {
|
|
if (window.MarkitectDebugSystem) {
|
|
const messages = window.MarkitectDebugSystem.getRecentMessages(50);
|
|
if (messages && messages.length > 0) {
|
|
// Show messages in reverse order (newest first)
|
|
const reversedMessages = [...messages].reverse();
|
|
container.innerHTML = `
|
|
<div style="padding: 0.25rem 0.5rem; background: #e9ecef; font-size: 0.7rem; color: #6c757d;">
|
|
${messages.length} messages (newest first) |
|
|
<span onclick="window.MarkitectDebugSystem.addMessage('Test message from debug control', 'INFO', 'DebugControl', {eventType: 'CONTROL'})"
|
|
style="cursor: pointer; text-decoration: underline;">Add Test</span>
|
|
</div>
|
|
${reversedMessages.map(msg => `
|
|
<div style="padding: 0.25rem 0.5rem; border-bottom: 1px solid #e9ecef;
|
|
color: ${this.getMessageColor(msg.category)};">
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
|
<div style="flex: 1;">
|
|
<small style="color: #6c757d;">[${msg.displayTime}]</small>
|
|
<span style="font-weight: bold; margin: 0 0.25rem 0 0.5rem;">${msg.category}:</span>
|
|
${msg.message}
|
|
</div>
|
|
${msg.source ? `<small style="color: #868e96; font-size: 0.7rem; margin-left: 0.5rem;">${msg.source}</small>` : ''}
|
|
</div>
|
|
${msg.context && msg.context.headingId ? `
|
|
<div style="font-size: 0.7rem; color: #6c757d; padding-left: 1rem;">
|
|
📍 Section: ${msg.context.headingId}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`).join('')}
|
|
`;
|
|
} else {
|
|
container.innerHTML = `
|
|
<div style="padding: 0.5rem; color: #666; text-align: center;">
|
|
No debug messages yet<br>
|
|
<button onclick="window.MarkitectDebugSystem.addMessage('First debug message', 'INFO')"
|
|
style="margin-top: 0.5rem; padding: 0.25rem 0.5rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
Add Test Message
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
container.innerHTML = `
|
|
<div style="padding: 0.5rem; color: #dc3545; text-align: center;">
|
|
Debug system not initialized
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (e) {
|
|
console.log("🪲 Error updating messages:", e);
|
|
container.innerHTML = `
|
|
<div style="padding: 0.5rem; color: #dc3545; text-align: center;">
|
|
Error loading messages: ${e.message}
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
|
|
// Add method to get message colors by type
|
|
debugControl.getMessageColor = function(type) {
|
|
const colors = {
|
|
'ERROR': '#dc3545',
|
|
'WARNING': '#fd7e14',
|
|
'SUCCESS': '#28a745',
|
|
'INFO': '#17a2b8',
|
|
'DEBUG': '#6f42c1'
|
|
};
|
|
return colors[type] || '#495057';
|
|
};
|
|
|
|
// Override collapse to clean up intervals
|
|
const originalCollapse = debugControl.collapse;
|
|
debugControl.collapse = function() {
|
|
if (this.messageUpdateInterval) {
|
|
clearInterval(this.messageUpdateInterval);
|
|
this.messageUpdateInterval = null;
|
|
}
|
|
originalCollapse.call(this);
|
|
};
|
|
|
|
// Create and show the debug control
|
|
console.log("🪲 Creating debug control...");
|
|
debugControl.createControl();
|
|
console.log("🪲 Debug control created, element:", debugControl.element);
|
|
|
|
// Make debug control globally accessible
|
|
window.debugControl = debugControl;
|
|
console.log("🪲 Debug control setup complete and globally accessible");
|
|
} catch (error) {
|
|
console.error("DebugControl failed to initialize:", error);
|
|
}
|
|
|
|
|
|
// 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 system
|
|
window.MarkitectDebugSystem?.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS', 'DocumentSaver', {eventType: 'DOCUMENT'});
|
|
console.log(`Document successfully saved as: ${editedFilename}`);
|
|
|
|
} catch (error) {
|
|
window.MarkitectDebugSystem?.addMessage(`Save failed: ${error.message}`, 'ERROR', 'DocumentSaver', {eventType: 'DOCUMENT'});
|
|
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);
|
|
window.MarkitectDebugSystem?.addMessage(`Reset all sections to original state`, 'INFO', 'SectionManager', {eventType: 'EDITING'});
|
|
}
|
|
});
|
|
|
|
// Set up debug system integration with section-aware logging
|
|
sectionManager.on('sections-created', (data) => {
|
|
window.MarkitectDebugSystem?.addMessage(`Created ${data.count} sections`, 'INFO', 'SectionManager', {eventType: 'DOCUMENT'});
|
|
});
|
|
|
|
sectionManager.on('edit-started', (data) => {
|
|
window.MarkitectDebugSystem?.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId});
|
|
});
|
|
|
|
sectionManager.on('changes-accepted', (data) => {
|
|
window.MarkitectDebugSystem?.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId});
|
|
// 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);
|
|
window.MarkitectDebugSystem?.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO', 'DOMRenderer', {eventType: 'EDITING', sectionId: data.sectionId});
|
|
}
|
|
}
|
|
});
|
|
|
|
sectionManager.on('changes-cancelled', (data) => {
|
|
window.MarkitectDebugSystem?.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId});
|
|
});
|
|
|
|
// Initialize with markdown content
|
|
const markdownToRender = markdownContent || '';
|
|
if (markdownToRender.trim()) {
|
|
const sections = sectionManager.createSectionsFromMarkdown(markdownToRender);
|
|
domRenderer.renderAllSections(sections);
|
|
window.MarkitectDebugSystem?.addMessage(`Initialized with ${sections.length} sections`, 'INFO', 'DocumentRenderer', {eventType: 'DOCUMENT'});
|
|
} else {
|
|
window.MarkitectDebugSystem?.addMessage('No markdown content to initialize', 'WARNING', 'DocumentRenderer', {eventType: 'DOCUMENT'});
|
|
}
|
|
|
|
// 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)
|