""" 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"""
${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 = `