""" 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"" 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'{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 = {{ 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""" {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.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(/]*)>/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 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(/
]*)>/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 = `

📝 Editor

Ready
`; // 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 }; } """