Files
markitect-main/markitect/clean_document_manager.py
tegwick c7a83070f8
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
feat: implement insert mode with heading protection and fix content display bugs
This commit implements a comprehensive insert mode that preserves document structure
by protecting heading levels 1-3 from modification while allowing full content editing.

## Insert Mode Features
- CLI integration with --insert flag for md-render command
- Protected heading display (read-only) for levels 1-3
- Content-only editing for sections with protected headings
- Full editing capability for heading levels 4-6
- Theme-aware CSS styling for all UI themes
- Modal confirmation dialogs with proper positioning
- Section splitting with automatic protection inheritance
- Validation to prevent protected heading modifications

## Implementation Details
- Added MARKITECT_INSERT_MODE JavaScript flag and configuration
- Enhanced Section class with heading level detection and protection methods
- Added getHeadingText() and getHeadingContent() methods for content separation
- Implemented insert mode UI with protected heading display above content editor
- Added comprehensive CSS styling for insert mode components and modals
- Updated CLI with --insert option and mutual exclusion with --edit

## Bug Fixes
- Fixed JavaScript syntax errors caused by unescaped newline characters in string literals
- Corrected split('\n') and join('\n') calls to use proper escaping for Python string context
- Fixed heading level 3 display showing "null" by improving regex pattern matching
- Resolved content not displaying in edit/insert modes due to JavaScript parsing failures

## Documentation
- Updated UserInterfaceFramework.md with complete Insert Mode Editor section
- Added behavioral comparison table between edit and insert modes
- Updated Component Integration Matrix to reflect new capabilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 23:55:21 +01:00

2678 lines
103 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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) -> 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
markdown_content = input_path.read_text(encoding='utf-8')
# 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
)
# 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 _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) -> 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)
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)
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)
def _generate_layered_css(self, properties: dict) -> 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_css = ""
if props['heading_style'] == 'underlined':
heading_css = f"""
h1, h2, h3, h4, h5, h6 {{
color: {props['heading_color']};
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']};
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']};
}}"""
# 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')};
}}
"""
return f"<style>{base_css}{heading_css}{text_css}{element_css}{link_css}{accent_css}{ui_css}</style>"
def _get_legacy_template_css(self, template: str) -> 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)
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) -> 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
# Escape the markdown content for JavaScript
js_markdown_content = json.dumps(markdown_content_with_dogtag)
# 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)
# 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()
# 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};
{editor_config}
{editor_scripts}
// Always render content first (graceful degradation)
document.addEventListener('DOMContentLoaded', function() {{
console.log("Rendering content...");
const contentDiv = document.getElementById('markdown-content');
// Step 1: Ensure content is always displayed
if (contentDiv) {{
if (typeof marked !== 'undefined') {{
try {{
const html = marked.parse(markdownContent);
// 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);
}}
}});
// 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:
"""Get the complete clean editor JavaScript code."""
return """
// Clean Editor Architecture
/**
* Test-Driven Section Editor Implementation
*
* A clean, object-oriented approach to handling section editing
* that can be tested independently of the DOM.
*/
// Enums for clear state management
const EditState = Object.freeze({
ORIGINAL: 'original',
EDITING: 'editing',
MODIFIED: 'modified',
SAVED: 'saved'
});
const SectionType = Object.freeze({
HEADING: 'heading',
PARAGRAPH: 'paragraph',
LIST: 'list',
CODE: 'code',
BLOCKQUOTE: 'blockquote'
});
/**
* Section class - Core business logic for a single editable section
*/
class Section {
constructor(id, originalMarkdown, sectionType = SectionType.PARAGRAPH) {
this.id = id;
this.originalMarkdown = originalMarkdown;
this.currentMarkdown = originalMarkdown;
this.editingMarkdown = null;
this.pendingMarkdown = null;
this.sectionType = sectionType;
this.headingLevel = Section.detectHeadingLevel(originalMarkdown);
this.state = EditState.ORIGINAL;
this.domElement = null;
this.lastSaved = null;
this.created = new Date();
}
startEdit() {
if (this.state === EditState.EDITING) {
throw new Error(`Section ${this.id} is already being edited`);
}
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
this.state = EditState.EDITING;
return this.editingMarkdown;
}
updateContent(markdown) {
if (this.state !== EditState.EDITING) {
throw new Error(`Section ${this.id} is not in editing state`);
}
this.editingMarkdown = markdown;
}
acceptChanges() {
if (this.state !== EditState.EDITING) {
throw new Error(`Section ${this.id} is not in editing state`);
}
this.currentMarkdown = this.editingMarkdown;
this.editingMarkdown = null;
this.pendingMarkdown = null;
this.state = EditState.SAVED;
this.lastSaved = new Date();
return this.currentMarkdown;
}
cancelChanges() {
if (this.state !== EditState.EDITING) {
throw new Error(`Section ${this.id} is not in editing state`);
}
this.editingMarkdown = null;
if (this.pendingMarkdown !== null) {
this.state = EditState.MODIFIED;
return this.pendingMarkdown;
} else if (this.lastSaved !== null) {
this.state = EditState.SAVED;
return this.currentMarkdown;
} else {
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
return this.currentMarkdown;
}
}
resetToOriginal() {
this.currentMarkdown = this.originalMarkdown;
this.editingMarkdown = null;
this.pendingMarkdown = null;
this.lastSaved = null;
this.state = EditState.ORIGINAL;
return this.originalMarkdown;
}
stopEditing() {
if (this.state !== EditState.EDITING) {
return this.state;
}
// If we have editing changes that differ from current content, preserve them as pending
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
this.pendingMarkdown = this.editingMarkdown;
this.state = EditState.MODIFIED; // Has pending changes
} else {
// No changes made during this edit session
this.pendingMarkdown = null;
if (this.lastSaved !== null) {
this.state = EditState.SAVED;
} else {
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
}
}
this.editingMarkdown = null;
return this.state;
}
hasChanges() {
return this.currentMarkdown !== this.originalMarkdown;
}
isEditing() {
return this.state === EditState.EDITING;
}
getStatus() {
return {
id: this.id,
state: this.state,
hasChanges: this.hasChanges(),
isEditing: this.isEditing(),
contentLength: this.currentMarkdown.length,
lastSaved: this.lastSaved,
sectionType: this.sectionType
};
}
static generateId(content, position) {
const str = content.substring(0, 100) + position.toString();
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return `section_${Math.abs(hash)}_${position}`;
}
static detectType(markdown) {
const trimmed = markdown.trim();
if (trimmed.startsWith('#')) return SectionType.HEADING;
if (trimmed.startsWith('```')) return SectionType.CODE;
if (trimmed.startsWith('>')) return SectionType.BLOCKQUOTE;
if (trimmed.startsWith('-') || trimmed.startsWith('*') || /^\\d+\\./.test(trimmed)) {
return SectionType.LIST;
}
return SectionType.PARAGRAPH;
}
static detectHeadingLevel(markdown) {
const trimmed = markdown.trim();
const match = trimmed.match(/^(#{1,6})\s/);
return match ? match[1].length : null;
}
isHeading() {
return this.sectionType === SectionType.HEADING;
}
isProtectedHeading() {
if (!this.isHeading()) return false;
// Check if we're in insert mode and if this heading level is protected
const config = window.editorConfig || {};
const restrictedLevels = config.restrictedHeadingLevels || [];
return config.mode === 'insert' && restrictedLevels.includes(this.headingLevel);
}
getHeadingText() {
if (!this.isHeading()) return null;
// Extract first line for heading text
const firstLine = this.originalMarkdown.trim().split('\\n')[0];
const match = firstLine.match(/^(#{1,6})\s+(.+)$/);
return match ? match[2] : null;
}
getHeadingContent() {
if (!this.isHeading()) return this.currentMarkdown;
const lines = this.currentMarkdown.split('\\n');
// Return content after the heading line
return lines.slice(1).join('\\n');
}
}
/**
* SectionManager class - Manages the collection of sections
*/
class SectionManager {
constructor() {
this.sections = new Map();
// Note: Removed single editingSection tracking to allow multiple concurrent edits
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => callback(data));
}
}
createSectionsFromMarkdown(markdownContent) {
const lines = markdownContent.split('\\n');
const sections = [];
let currentSection = '';
let position = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\s/.test(line);
const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
const isNewSection = isHeading || isNewParagraph;
if (isNewSection && currentSection.trim()) {
const sectionId = Section.generateId(currentSection, position);
const sectionType = Section.detectType(currentSection);
const section = new Section(sectionId, currentSection.trim(), sectionType);
sections.push(section);
this.sections.set(sectionId, section);
position++;
currentSection = line;
} else {
if (currentSection) currentSection += '\\n';
currentSection += line;
}
}
if (currentSection.trim()) {
const sectionId = Section.generateId(currentSection, position);
const sectionType = Section.detectType(currentSection);
const section = new Section(sectionId, currentSection.trim(), sectionType);
sections.push(section);
this.sections.set(sectionId, section);
}
this.emit('sections-created', { sections, count: sections.length });
return sections;
}
startEditing(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
// Check if section is already being edited
if (section.isEditing()) {
console.log('Section already in editing state:', sectionId);
return section.editingMarkdown;
}
const content = section.startEdit();
// Note: No longer tracking single editingSection - allowing multiple
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
return content;
}
updateContent(sectionId, markdown) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
section.updateContent(markdown);
this.emit('content-updated', { sectionId, markdown, section: section.getStatus() });
}
acceptChanges(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
// For protected headings in insert mode, validate that heading hasn't changed
if (section.isProtectedHeading()) {
const originalHeadingLine = section.originalMarkdown.split('\\n')[0];
const newHeadingLine = section.editingMarkdown.split('\\n')[0];
if (originalHeadingLine !== newHeadingLine) {
throw new Error(`Cannot modify protected heading in insert mode. Heading level ${section.headingLevel} is read-only.`);
}
}
// Check if the edited content contains new headings that would create splits
const newContent = section.editingMarkdown;
const originalContent = section.originalMarkdown;
const shouldSplit = this.checkForSectionSplits(newContent, originalContent);
if (shouldSplit) {
// Handle section splitting
this.handleSectionSplit(sectionId, newContent);
} else {
// Normal accept without splitting
const content = section.acceptChanges();
// Note: No longer tracking single editingSection
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
}
return section.currentMarkdown;
}
checkForSectionSplits(content, originalContent) {
if (!content) return false;
// Split by lines and check for headings
const lines = content.split('\\n');
const originalLines = originalContent ? originalContent.split('\\n') : [];
let newHeadingCount = 0;
let originalHeadingCount = 0;
// Count headings in new content
for (const line of lines) {
if (/^#{1,6}\s/.test(line.trim())) {
newHeadingCount++;
}
}
// Count headings in original content
for (const line of originalLines) {
if (/^#{1,6}\s/.test(line.trim())) {
originalHeadingCount++;
}
}
// Split if:
// 1. We have multiple headings now, OR
// 2. We added headings where there were none before, OR
// 3. We have more headings than we started with
return newHeadingCount > 1 ||
(originalHeadingCount === 0 && newHeadingCount > 0) ||
newHeadingCount > originalHeadingCount;
}
handleSectionSplit(originalSectionId, content) {
console.log('Splitting section:', originalSectionId);
const originalSection = this.sections.get(originalSectionId);
if (!originalSection) return;
// Accept the current changes first
originalSection.acceptChanges();
// Split the content into new sections
const newSections = this.createSectionsFromContent(content, originalSectionId);
// Get all sections as an ordered array to maintain document order
const allSectionsArray = Array.from(this.sections.values());
const originalIndex = allSectionsArray.findIndex(s => s.id === originalSectionId);
// Clear the sections map and rebuild it with proper order
this.sections.clear();
// Add sections before the original
for (let i = 0; i < originalIndex; i++) {
const section = allSectionsArray[i];
this.sections.set(section.id, section);
}
// Add the new split sections
newSections.forEach(section => {
this.sections.set(section.id, section);
});
// Add sections after the original
for (let i = originalIndex + 1; i < allSectionsArray.length; i++) {
const section = allSectionsArray[i];
this.sections.set(section.id, section);
}
// Note: No longer tracking single editingSection
// Emit event to trigger UI re-render
this.emit('section-split', {
originalSectionId,
newSections: newSections.map(s => s.getStatus()),
allSections: Array.from(this.sections.values())
});
}
createSectionsFromContent(content, baseSectionId) {
const lines = content.split('\\n');
const sections = [];
let currentSection = '';
let position = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\s/.test(line.trim());
if (isHeading) {
// When we encounter a heading, complete any previous section
if (currentSection.trim()) {
const sectionId = `${baseSectionId}_split_${position}`;
const sectionType = Section.detectType(currentSection);
const section = new Section(sectionId, currentSection.trim(), sectionType);
sections.push(section);
position++;
}
// Start new section with this heading
currentSection = line;
} else {
// Add content to current section
if (currentSection) currentSection += '\\n';
currentSection += line;
}
}
// Add the final section if it has content
if (currentSection.trim()) {
const sectionId = `${baseSectionId}_split_${position}`;
const sectionType = Section.detectType(currentSection);
const section = new Section(sectionId, currentSection.trim(), sectionType);
sections.push(section);
}
return sections;
}
cancelChanges(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const content = section.cancelChanges();
// Note: No longer tracking single editingSection
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
return content;
}
resetToOriginal(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const content = section.resetToOriginal();
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
return content;
}
stopEditing(sectionId) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const newState = section.stopEditing();
// Note: No longer tracking single editingSection
this.emit('edit-stopped', { sectionId, newState, section: section.getStatus() });
return newState;
}
getAllSections() {
return Array.from(this.sections.values());
}
getDocumentMarkdown() {
return this.getAllSections()
.map(section => section.currentMarkdown)
.join('\\n\\n');
}
}
/**
* DOM Renderer - Handles DOM interactions
*/
class DOMRenderer {
constructor(sectionManager, containerElement) {
this.sectionManager = sectionManager;
this.container = containerElement;
// Note: Removed single currentSection tracking to allow multiple concurrent edits
this.editingSections = new Set(); // Track multiple editing sections
this.handleSectionClick = this.handleSectionClick.bind(this);
this.handleAccept = this.handleAccept.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleReset = this.handleReset.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.setupEventListeners();
}
setupEventListeners() {
this.sectionManager.on('sections-created', (data) => {
this.renderAllSections(data.sections);
});
this.sectionManager.on('edit-started', (data) => {
this.showEditor(data.sectionId, data.content);
});
this.sectionManager.on('edit-stopped', (data) => {
this.hideEditor(data.sectionId);
// Don't update content - let pending changes remain
});
this.sectionManager.on('changes-accepted', (data) => {
this.hideEditor(data.sectionId);
this.updateSectionContent(data.sectionId, data.content);
});
this.sectionManager.on('changes-cancelled', (data) => {
this.hideEditor(data.sectionId);
this.updateSectionContent(data.sectionId, data.content);
});
this.sectionManager.on('section-reset', (data) => {
this.updateTextareaContent(data.content, data.sectionId);
});
this.sectionManager.on('section-split', (data) => {
console.log('Handling section split in UI');
this.handleSectionSplit(data);
});
}
renderAllSections(sections) {
this.container.innerHTML = '';
sections.forEach(section => {
const element = this.createSectionElement(section);
section.domElement = element;
this.container.appendChild(element);
});
this.container.addEventListener('click', this.handleSectionClick);
}
createSectionElement(section) {
const element = document.createElement('div');
element.setAttribute('data-section-id', section.id);
if (typeof marked !== 'undefined') {
const html = marked.parse(section.currentMarkdown);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
element.innerHTML = htmlWithTargetBlank;
} else {
element.innerHTML = `<p>${section.currentMarkdown}</p>`;
}
// Setup styling and event handlers
this.setupSectionElement(element);
return element;
}
handleSectionClick(event) {
// Don't handle clicks on form elements, buttons, or links
if (event.target.closest('textarea, button, input, a')) {
return;
}
const sectionElement = event.target.closest('.ui-edit-section');
if (!sectionElement) return;
const sectionId = sectionElement.getAttribute('data-section-id');
if (!sectionId) return;
// Check if this section is already being edited
const section = this.sectionManager.sections.get(sectionId);
if (section && section.isEditing()) {
console.log('Section already being edited:', sectionId);
return;
}
try {
console.log('Starting edit for section:', sectionId);
this.sectionManager.startEditing(sectionId);
} catch (error) {
console.error('Failed to start editing:', error);
}
}
showEditor(sectionId, content) {
const element = this.findSectionElement(sectionId);
if (!element) return;
this.hideCurrentEditor();
const section = this.sectionManager.sections.get(sectionId);
const isProtectedHeading = section && section.isProtectedHeading();
const editorContainer = document.createElement('div');
editorContainer.className = isProtectedHeading ? 'ui-edit-inline-panel ui-insert-protected-panel' : 'ui-edit-inline-panel';
editorContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
`;
// If this is a protected heading, show the heading display
if (isProtectedHeading) {
const headingDisplay = document.createElement('div');
headingDisplay.className = 'ui-insert-heading-display';
const headingText = section.getHeadingText();
const headingLevel = section.headingLevel;
const headingMarkdown = '#'.repeat(headingLevel) + ' ' + headingText;
headingDisplay.textContent = headingMarkdown;
headingDisplay.style.cssText = `
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 14px;
font-weight: bold;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
border-left: 4px solid #007bff;
color: #333;
`;
editorContainer.appendChild(headingDisplay);
}
// Create content editing area
const editingArea = document.createElement('div');
editingArea.style.cssText = `
display: flex;
gap: 12px;
align-items: flex-start;
`;
const textarea = document.createElement('textarea');
textarea.className = isProtectedHeading ? 'ui-edit-textarea ui-insert-content-editor' : 'ui-edit-textarea ui-edit-textarea-main';
// For protected headings, only show content after the heading
const textareaContent = isProtectedHeading ? section.getHeadingContent() : content;
textarea.value = textareaContent;
textarea.style.cssText = `
flex: 1;
min-height: 100px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
border-radius: 6px;
padding: 12px;
font-size: 14px;
line-height: 1.5;
resize: vertical;
`;
textarea.addEventListener('input', () => {
if (isProtectedHeading) {
// Reconstruct full content with protected heading
const headingLine = section.originalMarkdown.split('\\n')[0];
const fullContent = headingLine + '\\n' + textarea.value;
this.sectionManager.updateContent(sectionId, fullContent);
} else {
this.sectionManager.updateContent(sectionId, textarea.value);
}
});
textarea.addEventListener('keydown', this.handleKeydown);
const controls = document.createElement('div');
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 6px;
`;
const createButton = (text, className, handler) => {
const btn = document.createElement('button');
btn.textContent = text;
btn.className = className;
btn.addEventListener('click', handler);
return btn;
};
controls.appendChild(createButton('✓ Accept', 'ui-edit-button ui-edit-button-accept', () => this.handleAccept(sectionId)));
controls.appendChild(createButton('✗ Cancel', 'ui-edit-button ui-edit-button-cancel', () => this.handleCancel(sectionId)));
controls.appendChild(createButton('🔄 Reset', 'ui-edit-button ui-edit-button-reset', () => this.handleReset(sectionId)));
editingArea.appendChild(textarea);
editingArea.appendChild(controls);
editorContainer.appendChild(editingArea);
element.innerHTML = '';
element.appendChild(editorContainer);
textarea.focus();
// Track this section as being edited
this.editingSections.add(sectionId);
}
hideCurrentEditor() {
// This method is no longer needed since we support multiple editors
// Individual editors are hidden via hideEditor(sectionId)
}
hideEditor(sectionId) {
// Remove from editing sections set
this.editingSections.delete(sectionId);
// Force re-render the section to ensure it displays correctly
const section = this.sectionManager.sections.get(sectionId);
if (section) {
this.updateSectionContent(sectionId, section.currentMarkdown);
}
}
updateSectionContent(sectionId, content) {
const element = this.findSectionElement(sectionId);
if (!element) return;
if (typeof marked !== 'undefined') {
const html = marked.parse(content);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
element.innerHTML = htmlWithTargetBlank;
} else {
element.innerHTML = `<p>${content}</p>`;
}
// Restore the section styling and click behavior
this.setupSectionElement(element);
}
setupSectionElement(element) {
element.className = 'ui-edit-section';
element.style.cssText = `
margin: 16px 0;
padding: 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
`;
// Remove any existing event listeners to avoid duplicates
element.removeEventListener('mouseenter', element._mouseenterHandler);
element.removeEventListener('mouseleave', element._mouseleaveHandler);
// Create new handlers and store references
element._mouseenterHandler = () => {
element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
element.style.borderColor = 'rgba(0, 0, 0, 0.1)';
};
element._mouseleaveHandler = () => {
element.style.backgroundColor = '';
element.style.borderColor = 'transparent';
};
element.addEventListener('mouseenter', element._mouseenterHandler);
element.addEventListener('mouseleave', element._mouseleaveHandler);
}
updateTextareaContent(content, sectionId) {
// Find the specific textarea for this section
const element = this.findSectionElement(sectionId);
if (element) {
const textarea = element.querySelector('textarea');
if (textarea) {
textarea.value = content;
}
}
}
handleSectionSplit(data) {
// Clear the editor state for the original section
this.editingSections.delete(data.originalSectionId);
// Find the original section element and its position
const originalElement = this.findSectionElement(data.originalSectionId);
if (!originalElement) {
console.error('Original section element not found');
return;
}
// Get the position of the original element
const originalPosition = Array.from(this.container.children).indexOf(originalElement);
// Create new section elements for the split sections
const newElements = [];
data.newSections.forEach(sectionData => {
const section = this.sectionManager.sections.get(sectionData.id);
if (section) {
const element = this.createSectionElement(section);
section.domElement = element;
newElements.push(element);
}
});
// Remove the original element
originalElement.remove();
// Insert new elements at the original position
if (originalPosition < this.container.children.length) {
const nextElement = this.container.children[originalPosition];
newElements.forEach(element => {
this.container.insertBefore(element, nextElement);
});
} else {
// If original was at the end, just append
newElements.forEach(element => {
this.container.appendChild(element);
});
}
// Show success message
console.log(`Section split into ${data.newSections.length} sections`);
// Notify the main editor about the split
if (window.markitectCleanEditor) {
window.markitectCleanEditor.showMessage(
`✂️ Section split into ${data.newSections.length} sections!`,
'success'
);
}
}
findSectionElement(sectionId) {
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
}
handleAccept(sectionId) {
try {
console.log('Accepting changes for section:', sectionId);
this.sectionManager.acceptChanges(sectionId);
console.log('Changes accepted successfully');
} catch (error) {
console.error('Failed to accept changes:', error);
}
}
handleCancel(sectionId) {
try {
console.log('Canceling changes for section:', sectionId);
this.sectionManager.cancelChanges(sectionId);
console.log('Changes canceled successfully');
} catch (error) {
console.error('Failed to cancel changes:', error);
}
}
handleReset(sectionId) {
try {
this.sectionManager.resetToOriginal(sectionId);
} catch (error) {
console.error('Failed to reset section:', error);
}
}
handleKeydown(event) {
if (!this.currentSection) return;
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case 'Enter':
event.preventDefault();
this.handleAccept(this.currentSection);
break;
case 'Escape':
event.preventDefault();
this.handleCancel(this.currentSection);
break;
}
}
if (event.key === 'Escape') {
event.preventDefault();
this.handleCancel(this.currentSection);
}
}
}
/**
* Main Editor Integration
*/
class MarkitectCleanEditor {
constructor(markdownContent, containerElement, options = {}) {
this.options = {
theme: 'github',
keyboardShortcuts: true,
autosave: false,
...options
};
this.sectionManager = new SectionManager();
this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
this.originalMarkdown = markdownContent;
this.initialize();
}
initialize() {
try {
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
console.log(`✓ Initialized clean editor with ${sections.length} sections`);
// Add global control panel
this.addGlobalControls();
return true;
} catch (error) {
console.error('Failed to initialize clean editor:', error);
return false;
}
}
addGlobalControls() {
// Create floating control panel
const panel = document.createElement('div');
panel.id = 'ui-edit-floater';
panel.className = 'ui-edit-floater-panel';
panel.innerHTML = `
<div class="ui-edit-floater-header">
<h3>📝 Editor</h3>
<div class="ui-edit-floater-status" id="editor-status">Ready</div>
</div>
<div class="ui-edit-floater-actions">
<button id="save-document" class="ui-edit-button ui-edit-button-accept">💾 Save Document</button>
<button id="reset-all" class="ui-edit-button ui-edit-button-reset">🔄 Reset All</button>
<button id="show-status" class="ui-edit-button ui-edit-button-secondary">📊 Show Status</button>
</div>
`;
// Style the panel
panel.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
border-radius: 8px;
padding: 16px;
z-index: 1000;
font-family: system-ui, -apple-system, sans-serif;
min-width: 200px;
max-width: 250px;
`;
// Add internal styling for structural layout (theme colors come from CSS)
const style = document.createElement('style');
style.textContent = `
.ui-edit-floater-header h3 {
margin: 0 0 8px 0;
font-size: 16px;
}
.ui-edit-floater-status {
font-size: 12px;
margin-bottom: 12px;
}
.ui-edit-button {
display: block;
width: 100%;
margin: 6px 0;
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
border: 1px solid transparent;
}
`;
document.head.appendChild(style);
document.body.appendChild(panel);
// Add event listeners
document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
document.getElementById('show-status').addEventListener('click', () => this.showStatus());
// Update status periodically
this.statusInterval = setInterval(() => this.updateGlobalStatus(), 2000);
}
updateGlobalStatus() {
const statusEl = document.getElementById('editor-status');
if (!statusEl) return;
const sections = this.sectionManager.getAllSections();
const modified = sections.filter(s => s.hasChanges()).length;
const editing = sections.filter(s => s.isEditing()).length;
if (editing > 0) {
statusEl.textContent = `Editing ${editing} section${editing !== 1 ? 's' : ''}`;
statusEl.className = 'ui-edit-floater-status editing';
} else if (modified > 0) {
statusEl.textContent = `${modified} section${modified !== 1 ? 's' : ''} modified`;
statusEl.style.color = '';
} else {
statusEl.textContent = 'All sections saved ✓';
statusEl.style.color = '';
}
}
saveDocument() {
const markdown = this.getDocumentMarkdown();
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
// Generate intelligent filename
const filename = this.generateSaveFilename();
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('📄 Document saved as:', filename);
this.showMessage(`Document saved as: ${filename}`, 'success');
}
generateSaveFilename() {
// Try to get original filename from config
let baseName = 'document';
// Method 1: Use original filename from config if available
if (typeof MARKITECT_EDITOR_CONFIG !== 'undefined' && MARKITECT_EDITOR_CONFIG.originalFilename) {
baseName = MARKITECT_EDITOR_CONFIG.originalFilename;
}
// Method 2: Try to extract from page title
if (baseName === 'document') {
const title = document.title;
if (title && title !== 'Markdown Document' && !title.includes('Debug') && !title.includes('Test')) {
baseName = title.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Replace spaces with dashes
.replace(/-+/g, '-') // Collapse multiple dashes
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
}
}
// Method 3: Try to extract from URL pathname
if (baseName === 'document') {
const urlPath = window.location.pathname;
const match = urlPath.match(/\/([^\/]+)\.html?$/);
if (match) {
const urlBaseName = match[1];
if (!urlBaseName.includes('debug') && !urlBaseName.includes('test')) {
baseName = urlBaseName.replace(/_/g, '-');
}
}
}
// Method 4: Try to extract from first heading
if (baseName === 'document') {
const firstHeading = this.sectionManager.getAllSections()
.find(section => section.sectionType === 'heading');
if (firstHeading) {
baseName = firstHeading.originalMarkdown
.replace(/^#+\s*/, '') // Remove markdown heading syntax
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.substring(0, 30); // Limit length
}
}
// Generate timestamp
const now = new Date();
const timestamp = now.toISOString()
.replace(/T/, '-')
.replace(/:/g, '-')
.replace(/\.\d{3}Z$/, '');
// Check if there are modifications
const hasModifications = this.sectionManager.getAllSections()
.some(section => section.hasChanges());
if (hasModifications) {
return `${baseName}-edited-${timestamp}.md`;
} else {
return `${baseName}-${timestamp}.md`;
}
}
/**
* Show custom confirmation dialog with theme-consistent styling
* @param {string} message - The confirmation message
* @param {string} confirmText - Text for confirm button (default: "Confirm")
* @param {string} cancelText - Text for cancel button (default: "Cancel")
* @param {string} warningText - Optional warning text to highlight consequences
* @returns {Promise<boolean>} - True if confirmed, false if cancelled
*/
showConfirmation(message, confirmText = "Confirm", cancelText = "Cancel", warningText = null) {
return new Promise((resolve) => {
// Remove any existing modal
const existingModal = document.querySelector('.ui-edit-modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'ui-edit-modal-overlay';
// Create modal content
const modal = document.createElement('div');
modal.className = 'ui-edit-modal ui-edit-confirmation-modal';
// Create header
const header = document.createElement('div');
header.className = 'ui-edit-modal-header';
const title = document.createElement('h3');
title.className = 'ui-edit-modal-title';
title.textContent = 'Confirm Action';
const closeBtn = document.createElement('button');
closeBtn.className = 'ui-edit-modal-close';
closeBtn.innerHTML = '×';
closeBtn.setAttribute('aria-label', 'Close');
header.appendChild(title);
header.appendChild(closeBtn);
// Create body
const body = document.createElement('div');
body.className = 'ui-edit-modal-body';
const content = document.createElement('div');
content.className = 'ui-edit-confirmation-content';
content.textContent = message;
body.appendChild(content);
// Add warning section if provided
if (warningText) {
const warning = document.createElement('div');
warning.className = 'ui-edit-confirmation-warning';
warning.textContent = warningText;
body.appendChild(warning);
}
// Create footer with action buttons
const footer = document.createElement('div');
footer.className = 'ui-edit-modal-footer';
const buttonContainer = document.createElement('div');
buttonContainer.className = 'ui-edit-confirmation-buttons';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'ui-edit-button-cancel';
cancelBtn.textContent = cancelText;
const confirmBtn = document.createElement('button');
confirmBtn.className = 'ui-edit-button-confirm';
confirmBtn.textContent = confirmText;
buttonContainer.appendChild(cancelBtn);
buttonContainer.appendChild(confirmBtn);
footer.appendChild(buttonContainer);
// Assemble modal
modal.appendChild(header);
modal.appendChild(body);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Function to close modal and resolve
const closeModal = (result) => {
overlay.remove();
resolve(result);
};
// Event listeners
closeBtn.addEventListener('click', () => closeModal(false));
cancelBtn.addEventListener('click', () => closeModal(false));
confirmBtn.addEventListener('click', () => closeModal(true));
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeModal(false);
}
});
// Keyboard support
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
closeModal(false);
} else if (e.key === 'Enter') {
closeModal(true);
}
};
document.addEventListener('keydown', handleKeyDown);
// Clean up event listener when modal is closed
const originalResolve = resolve;
resolve = (result) => {
document.removeEventListener('keydown', handleKeyDown);
originalResolve(result);
};
// Show modal with animation
setTimeout(() => {
overlay.classList.add('active');
// Focus the confirm button for accessibility
confirmBtn.focus();
}, 10);
});
}
async resetAllSections() {
const confirmed = await this.showConfirmation(
'Reset all content to original markdown?',
'Reset Document',
'Keep Changes',
'This will permanently lose all edits and remove any split sections. This action cannot be undone.'
);
if (confirmed) {
// Clear the section manager completely
this.sectionManager.sections.clear();
// Note: No longer tracking single editingSection
// Recreate sections from original markdown
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
console.log('🔄 All sections reset to original structure');
this.showMessage('Document reset to original structure', 'info');
}
}
showStatus() {
const sections = this.sectionManager.getAllSections();
const total = sections.length;
const modified = sections.filter(s => s.hasChanges()).length;
const editing = sections.filter(s => s.isEditing()).length;
// Get the actual save filename that will be used
const saveFilename = this.generateSaveFilename();
// Create structured content for the modal
const modalContent = {
title: `📊 ${window.editorConfig.repoName} Status`,
sections: [
{
title: 'Application Information',
content: `${window.editorConfig.version}`
},
{
title: 'File Information',
content: `Save file: ${saveFilename}
Source: ${window.editorConfig.originalFilename}
URL: ${window.location.protocol}//${window.location.host}${window.location.pathname}`
},
{
title: 'Document Status',
content: `• Total sections: ${total}
• Modified sections: ${modified}
• Currently editing: ${editing}
• Unsaved changes: ${modified > 0 || editing > 0 ? 'Yes' : 'No'}`
},
{
title: 'Section Behavior',
content: `• Each section is a logical unit (heading + content until next heading)
• Content with line breaks stays in one section
• To split content: Create new headings (# ## ###)
• Sections don't auto-split on line breaks`
},
{
title: 'Editing Controls',
content: `• Click any section to edit its content
• Accept (✓) - Save changes to that section
• Cancel (✗) - Discard changes, return to previous state
• Reset (🔄) - Restore original content for that section
• Save Document - Download all current content
• Reset All - Restore entire document to original state`
}
]
};
this.showModal(modalContent);
}
showModal(content) {
// Remove any existing modal
const existingModal = document.querySelector('.ui-edit-modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'ui-edit-modal-overlay';
// Create modal content
const modal = document.createElement('div');
modal.className = 'ui-edit-modal';
// Create header
const header = document.createElement('div');
header.className = 'ui-edit-modal-header';
const title = document.createElement('h3');
title.className = 'ui-edit-modal-title';
title.textContent = content.title;
const closeBtn = document.createElement('button');
closeBtn.className = 'ui-edit-modal-close';
closeBtn.innerHTML = '×';
closeBtn.setAttribute('aria-label', 'Close');
header.appendChild(title);
header.appendChild(closeBtn);
// Create body
const body = document.createElement('div');
body.className = 'ui-edit-modal-body';
// Add sections
content.sections.forEach(section => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'ui-edit-modal-section';
const sectionTitle = document.createElement('div');
sectionTitle.className = 'ui-edit-modal-section-title';
sectionTitle.textContent = section.title;
const sectionContent = document.createElement('div');
sectionContent.className = 'ui-edit-modal-content';
sectionContent.textContent = section.content;
sectionDiv.appendChild(sectionTitle);
sectionDiv.appendChild(sectionContent);
body.appendChild(sectionDiv);
});
// Create footer with close button
const footer = document.createElement('div');
footer.className = 'ui-edit-modal-footer';
const footerCloseBtn = document.createElement('button');
footerCloseBtn.className = 'ui-edit-button ui-edit-button-accept';
footerCloseBtn.textContent = 'Close';
footer.appendChild(footerCloseBtn);
// Assemble modal
modal.appendChild(header);
modal.appendChild(body);
modal.appendChild(footer);
overlay.appendChild(modal);
// Add to page
document.body.appendChild(overlay);
// Close handlers
const closeModal = () => {
overlay.classList.remove('active');
setTimeout(() => {
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
}, 300);
};
closeBtn.addEventListener('click', closeModal);
footerCloseBtn.addEventListener('click', closeModal);
// Close on overlay click (but not modal content)
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeModal();
}
});
// Close on Escape key
const handleKeydown = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', handleKeydown);
}
};
document.addEventListener('keydown', handleKeydown);
// Show modal with animation
requestAnimationFrame(() => {
overlay.classList.add('active');
});
// Focus management
setTimeout(() => {
closeBtn.focus();
}, 100);
}
showMessage(message, type = 'info') {
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
const colors = {
'success': '#28a745',
'error': '#dc3545',
'info': '#007acc'
};
messageDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: ${colors[type] || colors.info};
color: white;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 10001;
font-size: 14px;
max-width: 400px;
text-align: center;
`;
document.body.appendChild(messageDiv);
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 3000);
}
getDocumentMarkdown() {
return this.sectionManager.getDocumentMarkdown();
}
}
// Initialize the clean editor system
let markitectCleanEditor;
function initializeCleanEditor() {
const container = document.getElementById('markdown-content');
if (!container) {
console.error('Markdown content container not found');
return;
}
if (typeof window.MarkitectEditor === 'undefined') {
console.error('MarkitectEditor not found');
return;
}
markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContent, container);
window.markitectCleanEditor = markitectCleanEditor; // Make globally available
console.log('✅ Clean section editor initialized successfully');
}
// Document scroll indicators
function initializeScrollIndicators() {
// Create scroll indicators
const scrollUpIndicator = document.createElement('div');
scrollUpIndicator.className = 'ui-scroll-indicator ui-scroll-indicator-up';
scrollUpIndicator.setAttribute('aria-label', 'Scroll to top');
scrollUpIndicator.setAttribute('title', 'Scroll up');
const scrollDownIndicator = document.createElement('div');
scrollDownIndicator.className = 'ui-scroll-indicator ui-scroll-indicator-down';
scrollDownIndicator.setAttribute('aria-label', 'Scroll to bottom');
scrollDownIndicator.setAttribute('title', 'Scroll down');
document.body.appendChild(scrollUpIndicator);
document.body.appendChild(scrollDownIndicator);
let scrollIndicatorTimeout = null;
// Function to show/hide indicators based on scroll position and mouse position
function updateScrollIndicators(mouseY = null) {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// Determine if scrolling is possible in each direction
const canScrollUp = scrollTop > 0;
const canScrollDown = scrollTop < documentHeight - windowHeight;
// Only show indicators if there's any scroll possibility or if document is short
let showUp = false;
let showDown = false;
// Show indicators on mouseover near top/bottom of viewport
if (mouseY !== null) {
const topZone = 100; // pixels from top
const bottomZone = windowHeight - 100; // pixels from bottom
if (mouseY <= topZone) {
showUp = true;
}
if (mouseY >= bottomZone) {
showDown = true;
}
}
// Update indicator visibility and state
if (showUp) {
scrollUpIndicator.classList.add('active');
if (canScrollUp) {
scrollUpIndicator.classList.remove('disabled');
} else {
scrollUpIndicator.classList.add('disabled');
}
} else {
scrollUpIndicator.classList.remove('active');
}
if (showDown) {
scrollDownIndicator.classList.add('active');
if (canScrollDown) {
scrollDownIndicator.classList.remove('disabled');
} else {
scrollDownIndicator.classList.add('disabled');
}
} else {
scrollDownIndicator.classList.remove('active');
}
// Auto-hide after a delay when mouse moves away
if (scrollIndicatorTimeout) {
clearTimeout(scrollIndicatorTimeout);
}
scrollIndicatorTimeout = setTimeout(() => {
scrollUpIndicator.classList.remove('active');
scrollDownIndicator.classList.remove('active');
}, 2000);
}
// Mouse move handler
function handleMouseMove(e) {
updateScrollIndicators(e.clientY);
}
// Smooth scroll function
function smoothScroll(targetY, duration = 500) {
const startY = window.pageYOffset;
const difference = targetY - startY;
const startTime = performance.now();
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function (ease-out)
const easeOut = 1 - Math.pow(1 - progress, 3);
window.scrollTo(0, startY + difference * easeOut);
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
// Click handlers for smooth scrolling
scrollUpIndicator.addEventListener('click', () => {
const currentScroll = window.pageYOffset;
const targetScroll = Math.max(0, currentScroll - window.innerHeight * 0.8);
smoothScroll(targetScroll);
});
scrollDownIndicator.addEventListener('click', () => {
const currentScroll = window.pageYOffset;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const targetScroll = Math.min(maxScroll, currentScroll + window.innerHeight * 0.8);
smoothScroll(targetScroll);
});
// Event listeners
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('scroll', () => updateScrollIndicators());
// Initial check
updateScrollIndicators();
console.log('✅ Document scroll indicators initialized');
}
// Export for testing and usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
} else {
window.MarkitectEditor = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
}
"""