"""
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, 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,
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')};
}}
"""
return f""
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, 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'{username}'
else:
username_link = username
dogtag = f'\n\n---\n*-- html from markdown by MarkiTect 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""
else:
css_content = f''
except Exception:
css_content = f''
# 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 = {{
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
editor_scripts = self._get_clean_editor_scripts()
# Generate the complete HTML template
html_template = f"""
{title}
{css_content}
{default_css}
"""
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.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;
}
}
/**
* 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`);
}
// 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(/]*)>/g, '');
element.innerHTML = htmlWithTargetBlank;
} else {
element.innerHTML = `${section.currentMarkdown}
`;
}
// 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 editorContainer = document.createElement('div');
editorContainer.className = 'ui-edit-inline-panel';
editorContainer.style.cssText = `
display: flex;
gap: 12px;
align-items: flex-start;
width: 100%;
`;
const textarea = document.createElement('textarea');
textarea.className = 'ui-edit-textarea ui-edit-textarea-main';
textarea.value = content;
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', () => {
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)));
editorContainer.appendChild(textarea);
editorContainer.appendChild(controls);
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(/]*)>/g, '');
element.innerHTML = htmlWithTargetBlank;
} else {
element.innerHTML = `${content}
`;
}
// 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 = `
`;
// 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} - 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 };
}
"""