""" Clean Document Manager - Simplified version with only clean editor support """ import json import re from pathlib import Path from typing import Dict, Any, Optional class CleanDocumentManager: """ Simplified document manager that only supports the clean editor implementation. All legacy code has been removed for clarity and maintainability. """ def __init__(self, db_manager=None): self.db_manager = db_manager def store_document(self, file_path: str, content: str, ast: list = None, front_matter: dict = None): """Store a document in the database.""" if self.db_manager: from pathlib import Path filename = Path(file_path).name return self.db_manager.store_markdown_file(filename, content) def get_file(self, file_path: str) -> Dict[str, Any]: """ Retrieve a markdown file from the database. Args: file_path: Path to the markdown file to retrieve Returns: Dictionary containing file content and metadata Raises: FileNotFoundError: If file is not found in database """ if not self.db_manager: raise ValueError("Database manager not initialized") # Get file from database file_data = self.db_manager.get_markdown_file(file_path) if file_data is None: raise FileNotFoundError(f"File '{file_path}' not found in database") return { 'content': file_data.get('content', ''), 'metadata': { 'filename': file_data.get('filename', file_path), 'front_matter': file_data.get('front_matter'), 'size': len(file_data.get('content', '')), 'modified': file_data.get('modified') } } def render_file(self, input_file: str, output_file: str, template: str = None, css: str = None, edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, nodogtag: bool = False, image_max_width: str = '12cm', image_max_height: str = '20cm') -> Dict[str, Any]: """ Render a markdown file to HTML with optional clean editing capabilities. """ input_path = Path(input_file) output_path = Path(output_file) if not input_path.exists(): raise FileNotFoundError(f"Input file not found: {input_file}") # Read markdown content raw_markdown_content = input_path.read_text(encoding='utf-8') # Process base64 images - relocate payloads to document end markdown_content, base64_references = self._process_base64_images(raw_markdown_content) # Extract title from markdown (first h1 heading) title = self._extract_title_from_markdown(markdown_content) # Get original filename without extension original_filename = input_path.stem # Get version information version_info = self._get_version_info() # Generate HTML content html_content = self._generate_html_template( markdown_content=markdown_content, title=title, css=css, template=template, edit_mode=edit_mode, insert_mode=insert_mode, editor_theme=editor_theme, keyboard_shortcuts=keyboard_shortcuts, original_filename=original_filename, version_info=version_info, nodogtag=nodogtag, image_max_width=image_max_width, image_max_height=image_max_height, base64_references=base64_references ) # Write HTML file output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(html_content, encoding='utf-8') return { 'success': True, 'input_file': str(input_path), 'output_file': str(output_path), 'edit_mode': edit_mode, 'editor_theme': editor_theme } def _extract_title_from_markdown(self, markdown_content: str) -> str: """Extract title from first h1 heading in markdown.""" match = re.search(r'^#\s+(.+)', markdown_content, re.MULTILINE) if match: return match.group(1).strip() return "Markdown Document" def _process_base64_images(self, markdown_content: str) -> tuple: """ Process base64 encoded images in markdown content. - Extracts base64 image data URLs - Replaces them with reference links - Returns processed content and reference mapping Returns: tuple: (processed_markdown, base64_references_dict) """ import re import uuid # Pattern to match base64 image data URLs base64_pattern = r'!\[([^\]]*)\]\(data:image/([^;]+);base64,([^)]+)\)' base64_references = {} reference_definitions = [] processed_content = markdown_content # Find all base64 images matches = list(re.finditer(base64_pattern, markdown_content)) for i, match in enumerate(matches): alt_text = match.group(1) image_type = match.group(2) # png, jpeg, svg+xml, etc. base64_data = match.group(3) # Generate a unique reference ID ref_id = f"base64-image-{i+1}" # Store the mapping base64_references[ref_id] = { 'alt': alt_text, 'type': image_type, 'data': base64_data, 'full_data_url': f"data:image/{image_type};base64,{base64_data}" } # Replace the inline base64 with reference original_match = match.group(0) reference_link = f"![{alt_text}][{ref_id}]" processed_content = processed_content.replace(original_match, reference_link, 1) # Create reference definition for the end of document reference_definitions.append(f"[{ref_id}]: data:image/{image_type};base64,{base64_data}") # Add reference definitions to the end of the document if any base64 images were found if reference_definitions: # Ensure there's a blank line before the references if not processed_content.endswith('\n\n'): if processed_content.endswith('\n'): processed_content += '\n' else: processed_content += '\n\n' # Add a comment to indicate the base64 reference section processed_content += "\n" processed_content += '\n'.join(reference_definitions) + '\n' return processed_content, base64_references def _get_version_info(self) -> dict: """Get repository name and version information.""" from .__version__ import get_version_info version_info = get_version_info() # Transform to the format expected by the editor return { 'repo_name': 'Markitect', 'version': version_info['full_version'], 'git_info': '' # Already included in full_version } def _get_template_css(self, template: str = None, image_max_width: str = '12cm', image_max_height: str = '20cm') -> str: """Generate layered theme CSS styles.""" # Import layered theme functions from markitect.plugins.builtin.markdown_commands import ( parse_theme_string, combine_theme_properties, TEMPLATE_STYLES ) # Handle layered themes or fall back to legacy if template and ',' in template: # New layered theme system theme_list = parse_theme_string(template) combined_props = combine_theme_properties(theme_list) return self._generate_layered_css(combined_props, image_max_width, image_max_height) else: # Legacy single theme or fallback if not template or template not in TEMPLATE_STYLES: # Use default layered themes or the specified theme theme_list = parse_theme_string(template or 'basic') combined_props = combine_theme_properties(theme_list) return self._generate_layered_css(combined_props, image_max_width, image_max_height) else: # Legacy theme - convert to layered theme_list = parse_theme_string(template) combined_props = combine_theme_properties(theme_list) return self._generate_layered_css(combined_props, image_max_width, image_max_height) def _generate_layered_css(self, properties: dict, image_max_width: str = '12cm', image_max_height: str = '20cm') -> str: """Generate CSS from combined theme properties.""" # Set defaults for missing properties (properties override defaults) defaults = { 'body_background': '#ffffff', 'body_color': '#333333', 'font_family': '-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif', 'max_width': '800px', 'heading_color': '#333333', # Use same as body color by default 'heading_style': 'simple', 'text_align': 'left', 'code_background': '#f6f8fa', 'code_color': '#333333', 'border_color': '#d0d7de', 'blockquote_border': '#dfe2e5', 'blockquote_color': '#6a737d', 'table_border': '#d0d7de', 'table_header_bg': '#f6f8fa', 'accent_color': None, 'secondary_color': None } # Merge defaults first, then override with theme properties props = {**defaults, **properties} # Base CSS base_css = f""" body {{ font-family: {props['font_family']}; max-width: {props['max_width']}; margin: 0 auto; padding: 2rem; line-height: 1.6; color: {props['body_color']}; background-color: {props['body_background']}; }} #markdown-content {{ min-height: 200px; }}""" # Heading styles heading_font_style = "" if 'heading_font_family' in props and props['heading_font_family']: heading_font_style = f"font-family: {props['heading_font_family']};" heading_css = "" if props['heading_style'] == 'underlined': heading_css = f""" h1, h2, h3, h4, h5, h6 {{ color: {props['heading_color']}; {heading_font_style} border-bottom: 1px solid {props['border_color']}; padding-bottom: 0.3em; }}""" elif props['heading_style'] == 'centered': heading_css = f""" h1, h2, h3, h4, h5, h6 {{ color: {props['heading_color']}; {heading_font_style} margin-top: 2rem; margin-bottom: 1rem; }} h1 {{ text-align: center; font-size: 2.2em; border-bottom: 2px solid {props['heading_color']}; padding-bottom: 0.5rem; }}""" else: # simple heading_css = f""" h1, h2, h3, h4, h5, h6 {{ color: {props['heading_color']}; {heading_font_style} }}""" # Text alignment text_css = "" if props['text_align'] == 'justify': text_css = """ p { text-align: justify; margin-bottom: 1.2rem; }""" # Element styling element_css = f""" pre {{ background-color: {props['code_background']}; color: {props['code_color']}; padding: 1rem; border-radius: 6px; overflow-x: auto; border: 1px solid {props['border_color']}; }} code {{ background-color: {props['code_background']}; color: {props['code_color']}; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }} pre code {{ background: none; padding: 0; }} blockquote {{ border-left: 4px solid {props['blockquote_border']}; margin: 0; padding-left: 1rem; color: {props['blockquote_color']}; }} table {{ font-size: 0.85em; border-collapse: collapse; margin: 1rem 0; width: 100%; border: 1px solid {props['table_border']}; }} th, td {{ font-size: inherit; border: 1px solid {props['table_border']}; padding: 0.5rem; text-align: left; }} th {{ background-color: {props['table_header_bg']}; font-weight: 600; }}""" # Link styling link_css = "" if props.get('link_color'): link_css = f""" a {{ color: {props['link_color']}; text-decoration: underline; }}""" if props.get('link_hover_color'): link_css += f""" a:hover {{ color: {props['link_hover_color']}; }}""" else: link_css += """ a:hover { opacity: 0.8; }""" # Branding accents (if specified and no link_color already set) accent_css = "" if props.get('accent_color') and not props.get('link_color'): accent_css = f""" a {{ color: {props['accent_color']}; }} a:hover {{ opacity: 0.8; }}""" # UI theme styling for editor interface elements ui_css = "" if props.get('editor_panel_bg'): ui_css = f""" .markitect-edit-mode .ui-edit-floater-panel {{ background: {props['editor_panel_bg']}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')}; color: {props.get('editor_text_color', '#212529')}; }} .markitect-edit-mode .ui-edit-floater-header {{ color: {props.get('editor_text_color', '#212529')}; }} .markitect-edit-mode .ui-edit-floater-header h3 {{ color: {props.get('editor_text_color', '#212529')}; }} .markitect-edit-mode .ui-edit-inline-panel {{ background: {props['editor_panel_bg']}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; box-shadow: 0 2px 8px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')}; color: {props.get('editor_text_color', '#212529')}; border-radius: 8px; padding: 12px; margin: 8px 0; }} .markitect-edit-mode .ui-edit-button {{ background: {props.get('editor_button_bg', '#ffffff')}; color: {props.get('editor_text_color', '#212529')}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; min-width: 70px; font-weight: 500; transition: all 0.2s; }} .markitect-edit-mode .ui-edit-button:hover {{ background: {props.get('editor_button_hover', '#e9ecef')}; }} .markitect-edit-mode .ui-edit-button:active, .markitect-edit-mode .ui-edit-button.active {{ background: {props.get('editor_button_active', '#dee2e6')}; }} .markitect-edit-mode .ui-edit-button-accept {{ background: {props.get('editor_accept_bg', '#4caf50')}; color: white; }} .markitect-edit-mode .ui-edit-button-accept:hover {{ background: {props.get('editor_accept_hover', '#388e3c')}; }} .markitect-edit-mode .ui-edit-button-cancel {{ background: {props.get('editor_cancel_bg', '#f44336')}; color: white; }} .markitect-edit-mode .ui-edit-button-cancel:hover {{ background: {props.get('editor_cancel_hover', '#d32f2f')}; }} .markitect-edit-mode .ui-edit-button-reset {{ background: {props.get('editor_reset_bg', '#ff9800')}; color: white; }} .markitect-edit-mode .ui-edit-button-reset:hover {{ background: {props.get('editor_reset_hover', '#f57c00')}; }} .markitect-edit-mode .ui-edit-button-secondary {{ background: {props.get('editor_secondary_bg', '#6c757d')}; color: white; }} .markitect-edit-mode .ui-edit-button-secondary:hover {{ background: {props.get('editor_secondary_hover', '#545b62')}; }} .markitect-edit-mode .ui-edit-section-frame {{ border: 2px solid {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}; box-shadow: 0 0 0 3px {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}33; }} .markitect-edit-mode .ui-edit-textarea {{ border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; color: {props.get('editor_text_color', '#212529')}; background: {props.get('editor_button_bg', '#ffffff')}; }} .markitect-edit-mode .ui-edit-textarea:focus {{ border-color: {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}; box-shadow: 0 0 0 2px {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}33; }} .markitect-edit-mode .ui-edit-modal-overlay {{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 999; display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; }} .markitect-edit-mode .ui-edit-modal-overlay.active {{ opacity: 1; visibility: visible; }} .markitect-edit-mode .ui-edit-modal {{ background: {props['editor_panel_bg']}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; box-shadow: 0 8px 32px {props.get('editor_shadow', 'rgba(0,0,0,0.2)')}; color: {props.get('editor_text_color', '#212529')}; border-radius: 8px; max-width: 600px; max-height: 80vh; width: 90%; overflow: hidden; transform: scale(0.9) translateY(-20px); transition: transform 0.3s; }} .markitect-edit-mode .ui-edit-modal-overlay.active .ui-edit-modal {{ transform: scale(1) translateY(0); }} .markitect-edit-mode .ui-edit-modal-header {{ padding: 20px 24px 16px; border-bottom: 1px solid {props.get('editor_panel_border', '#dee2e6')}; display: flex; justify-content: space-between; align-items: center; }} .markitect-edit-mode .ui-edit-modal-title {{ margin: 0; font-size: 18px; font-weight: 600; color: {props.get('editor_text_color', '#212529')}; }} .markitect-edit-mode .ui-edit-modal-close {{ background: none; border: none; font-size: 24px; cursor: pointer; color: {props.get('editor_text_color', '#212529')}; padding: 4px; border-radius: 4px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; }} .markitect-edit-mode .ui-edit-modal-close:hover {{ background: {props.get('editor_button_hover', '#e9ecef')}; }} .markitect-edit-mode .ui-edit-modal-body {{ padding: 20px 24px; overflow-y: auto; max-height: 60vh; }} .markitect-edit-mode .ui-edit-modal-content {{ white-space: pre-line; line-height: 1.5; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; font-size: 14px; }} .markitect-edit-mode .ui-edit-modal-section {{ margin-bottom: 16px; }} .markitect-edit-mode .ui-edit-modal-section:last-child {{ margin-bottom: 0; }} .markitect-edit-mode .ui-edit-modal-section-title {{ font-weight: 600; margin-bottom: 8px; color: {props.get('editor_text_color', '#212529')}; }} .markitect-edit-mode .ui-edit-modal-footer {{ padding: 16px 24px 20px; border-top: 1px solid {props.get('editor_panel_border', '#dee2e6')}; text-align: right; }} /* Confirmation Dialog Styles */ .markitect-edit-mode .ui-edit-confirmation-modal {{ max-width: 500px; }} .markitect-edit-mode .ui-edit-confirmation-content {{ font-size: 16px; line-height: 1.5; margin-bottom: 24px; color: {props.get('editor_text_color', '#212529')}; }} .markitect-edit-mode .ui-edit-confirmation-warning {{ background: {props.get('editor_warning_bg', '#fff3cd')}; border: 1px solid {props.get('editor_warning_border', '#ffeaa7')}; color: {props.get('editor_warning_text', '#856404')}; padding: 12px 16px; border-radius: 6px; margin: 16px 0; font-size: 14px; }} .markitect-edit-mode .ui-edit-confirmation-buttons {{ display: flex; gap: 12px; justify-content: flex-end; }} .markitect-edit-mode .ui-edit-button-confirm {{ background: {props.get('editor_danger_button', '#dc3545')}; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s, transform 0.1s; }} .markitect-edit-mode .ui-edit-button-confirm:hover {{ background: {props.get('editor_danger_button_hover', '#c82333')}; transform: translateY(-1px); }} .markitect-edit-mode .ui-edit-button-confirm:active {{ transform: translateY(0); }} .markitect-edit-mode .ui-edit-button-confirm:focus {{ outline: 2px solid {props.get('editor_focus_color', '#007bff')}; outline-offset: 2px; }} .markitect-edit-mode .ui-edit-button-cancel {{ background: {props.get('editor_secondary_button', '#6c757d')}; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s, transform 0.1s; }} .markitect-edit-mode .ui-edit-button-cancel:hover {{ background: {props.get('editor_secondary_button_hover', '#545b62')}; transform: translateY(-1px); }} .markitect-edit-mode .ui-edit-button-cancel:active {{ transform: translateY(0); }} .markitect-edit-mode .ui-edit-button-cancel:focus {{ outline: 2px solid {props.get('editor_focus_color', '#007bff')}; outline-offset: 2px; }} /* Document Scroll Indicators */ .ui-scroll-indicator {{ position: fixed; left: 50%; transform: translateX(-50%); width: 60px; height: 30px; background: {props.get('editor_panel_bg', '#f8f9fa')}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; border-radius: 15px; display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.2s ease, background-color 0.2s ease; z-index: 1000; box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.15)')}; }} .ui-scroll-indicator:hover {{ transform: translateX(-50%) scale(1.05); }} .ui-scroll-indicator:not(.disabled):hover {{ background: {props.get('editor_button_hover', '#e9ecef')}; }} .ui-scroll-indicator.active {{ opacity: 0.9; visibility: visible; }} .ui-scroll-indicator.disabled {{ background: {props.get('editor_button_active', '#dee2e6')}; cursor: not-allowed; opacity: 0.6; }} .ui-scroll-indicator.disabled:hover {{ transform: translateX(-50%); background: {props.get('editor_button_active', '#dee2e6')}; }} .ui-scroll-indicator-up {{ top: 20px; }} .ui-scroll-indicator-down {{ bottom: 20px; }} .ui-scroll-indicator::before {{ content: ''; width: 0; height: 0; border-style: solid; transition: border-color 0.2s ease; }} .ui-scroll-indicator-up::before {{ border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 12px solid {props.get('editor_text_color', '#212529')}; }} .ui-scroll-indicator-down::before {{ border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 12px solid {props.get('editor_text_color', '#212529')}; }} .ui-scroll-indicator.disabled.ui-scroll-indicator-up::before {{ border-bottom-color: {props.get('editor_secondary_button', '#6c757d')}; }} .ui-scroll-indicator.disabled.ui-scroll-indicator-down::before {{ border-top-color: {props.get('editor_secondary_button', '#6c757d')}; }} /* Insert Mode Specific Styles */ .markitect-insert-mode .ui-edit-floater-panel {{ background: {props['editor_panel_bg']}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')}; color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-floater-header {{ color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-floater-header h3 {{ color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-inline-panel {{ background: {props['editor_panel_bg']}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; box-shadow: 0 2px 8px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')}; color: {props.get('editor_text_color', '#212529')}; border-radius: 8px; padding: 12px; margin: 8px 0; }} .markitect-insert-mode .ui-insert-protected-panel {{ border-left: 4px solid #ff9800; }} .markitect-insert-mode .ui-insert-heading-display {{ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 14px; font-weight: bold; padding: 8px 12px; background: {props.get('editor_warning_bg', '#fff3cd')}; border: 1px solid {props.get('editor_warning_border', '#ffeaa7')}; border-radius: 4px; border-left: 4px solid #007bff; color: {props.get('editor_warning_text', '#856404')}; margin-bottom: 8px; }} .markitect-insert-mode .ui-insert-content-editor {{ border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; color: {props.get('editor_text_color', '#212529')}; background: {props.get('editor_button_bg', '#ffffff')}; }} .markitect-insert-mode .ui-insert-content-editor:focus {{ border-color: {props.get('editor_focus_color', '#007bff')}; box-shadow: 0 0 0 2px {props.get('editor_focus_color', '#007bff')}33; }} .markitect-insert-mode .ui-edit-button {{ background: {props.get('editor_button_bg', '#ffffff')}; color: {props.get('editor_text_color', '#212529')}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; min-width: 70px; font-weight: 500; transition: all 0.2s; }} .markitect-insert-mode .ui-edit-button:hover {{ background: {props.get('editor_button_hover', '#e9ecef')}; }} .markitect-insert-mode .ui-edit-button:active, .markitect-insert-mode .ui-edit-button.active {{ background: {props.get('editor_button_active', '#dee2e6')}; }} .markitect-insert-mode .ui-edit-button-accept {{ background: #4caf50; color: white; }} .markitect-insert-mode .ui-edit-button-accept:hover {{ background: #388e3c; }} .markitect-insert-mode .ui-edit-button-cancel {{ background: #f44336; color: white; }} .markitect-insert-mode .ui-edit-button-cancel:hover {{ background: #d32f2f; }} .markitect-insert-mode .ui-edit-button-reset {{ background: #ff9800; color: white; }} .markitect-insert-mode .ui-edit-button-reset:hover {{ background: #f57c00; }} .markitect-insert-mode .ui-edit-section-frame {{ border: 2px solid {props.get('editor_focus_color', '#007bff')}; box-shadow: 0 0 0 3px {props.get('editor_focus_color', '#007bff')}33; }} /* Modal Overlay and Dialog Styles for Insert Mode */ .markitect-insert-mode .ui-edit-modal-overlay {{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 999; display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; }} .markitect-insert-mode .ui-edit-modal-overlay.active {{ opacity: 1; visibility: visible; }} .markitect-insert-mode .ui-edit-modal {{ background: {props['editor_panel_bg']}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; box-shadow: 0 8px 32px {props.get('editor_shadow', 'rgba(0,0,0,0.2)')}; color: {props.get('editor_text_color', '#212529')}; border-radius: 8px; max-width: 600px; max-height: 80vh; width: 90%; overflow: hidden; transform: scale(0.9) translateY(-20px); transition: transform 0.3s; }} .markitect-insert-mode .ui-edit-modal-overlay.active .ui-edit-modal {{ transform: scale(1) translateY(0); }} .markitect-insert-mode .ui-edit-modal-header {{ padding: 20px 24px 16px; border-bottom: 1px solid {props.get('editor_panel_border', '#dee2e6')}; display: flex; justify-content: space-between; align-items: center; }} .markitect-insert-mode .ui-edit-modal-title {{ margin: 0; font-size: 18px; font-weight: 600; color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-modal-close {{ background: transparent; border: none; font-size: 24px; cursor: pointer; color: {props.get('editor_text_color', '#212529')}; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: background-color 0.2s; }} .markitect-insert-mode .ui-edit-modal-close:hover {{ background: {props.get('editor_button_hover', '#e9ecef')}; }} .markitect-insert-mode .ui-edit-modal-body {{ padding: 20px 24px; max-height: 400px; overflow-y: auto; color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-modal-section {{ margin-bottom: 8px; color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-modal-footer {{ padding: 16px 24px 20px; border-top: 1px solid {props.get('editor_panel_border', '#dee2e6')}; text-align: right; }} /* Confirmation Dialog Styles for Insert Mode */ .markitect-insert-mode .ui-edit-confirmation-modal {{ max-width: 500px; }} .markitect-insert-mode .ui-edit-confirmation-content {{ font-size: 16px; line-height: 1.5; margin-bottom: 24px; color: {props.get('editor_text_color', '#212529')}; }} .markitect-insert-mode .ui-edit-confirmation-warning {{ background: {props.get('editor_warning_bg', '#fff3cd')}; color: {props.get('editor_warning_text', '#856404')}; border: 1px solid {props.get('editor_warning_border', '#ffeaa7')}; border-radius: 6px; padding: 12px 16px; margin: 16px 0; font-size: 14px; line-height: 1.4; }} .markitect-insert-mode .ui-edit-confirmation-buttons {{ display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }} .markitect-insert-mode .ui-edit-button-confirm {{ background: #dc3545; color: white; border: 1px solid #dc3545; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }} .markitect-insert-mode .ui-edit-button-confirm:hover {{ background: #c82333; border-color: #bd2130; }} .markitect-insert-mode .ui-edit-button-cancel {{ background: {props.get('editor_button_bg', '#ffffff')}; color: {props.get('editor_text_color', '#212529')}; border: 1px solid {props.get('editor_panel_border', '#dee2e6')}; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }} .markitect-insert-mode .ui-edit-button-cancel:hover {{ background: {props.get('editor_button_hover', '#e9ecef')}; }} """ # Image size CSS with configurable limits image_css = f""" img {{ max-width: {image_max_width}; max-height: {image_max_height}; height: auto; display: block; margin: 1rem auto; }}""" return f"" def _get_legacy_template_css(self, template: str, image_max_width: str = '12cm', image_max_height: str = '20cm') -> str: """Legacy CSS generation - kept for backward compatibility.""" # Import template styles from markitect.plugins.builtin.markdown_commands import TEMPLATE_STYLES # Use basic as default if no template specified if not template or template not in TEMPLATE_STYLES: template = 'basic' style_config = TEMPLATE_STYLES[template] # Base CSS that's common to all templates base_css = f""" body {{ font-family: {style_config['font_family']}; max-width: {style_config['max_width']}; margin: 0 auto; padding: 2rem; line-height: 1.6; color: {style_config['body_color']}; }} #markdown-content {{ min-height: 200px; }}""" # Convert legacy template config to layered format legacy_config = TEMPLATE_STYLES[template] layered_props = { 'font_family': legacy_config['font_family'], 'max_width': legacy_config['max_width'], 'body_color': legacy_config['body_color'], } return self._generate_layered_css(layered_props, image_max_width, image_max_height) def _generate_html_template(self, markdown_content: str, title: str, css: str = None, template: str = None, edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None, nodogtag: bool = False, image_max_width: str = '12cm', image_max_height: str = '20cm', base64_references: dict = None) -> str: """Generate clean HTML template using external template file.""" # Check if edit/insert mode components are available if edit_mode or insert_mode: mode_name = "insert mode" if insert_mode else "edit mode" # Check if required components exist components_to_check = [ 'js/core/section-manager.js', 'js/components/debug-panel.js', 'js/components/document-controls.js', 'js/components/dom-renderer.js' ] base_path = Path(__file__).parent / 'static' missing_components = [] for component_path in components_to_check: script_path = base_path / component_path if not script_path.exists(): missing_components.append(component_path) if missing_components: error_msg = f""" ⚠️ WARNING: {mode_name.title()} requested but some components are missing! Missing components: {chr(10).join(f' - {comp}' for comp in missing_components)} The system will attempt to load {mode_name} but some functionality may be broken. RECOMMENDATIONS: 1. Restore missing components from git: git show HEAD:markitect/static/js/... 2. Use static mode instead: Remove --edit or --insert flag 3. Check if all editor components were properly restored FILE: {original_filename} MODE REQUESTED: {mode_name} MISSING: {len(missing_components)} components """ print(error_msg) # In strict mode, fail fast for missing components if self._should_fail_fast(): raise FileNotFoundError(f"{mode_name.title()} components missing: {', '.join(missing_components)}") else: print(f"✅ {mode_name.title()} components found - proceeding with interactive editing") # 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 dogtag = "" # Choose template based on mode if edit_mode or insert_mode: return self._generate_clean_edit_mode_html( markdown_content=markdown_content, markdown_content_with_dogtag=markdown_content_with_dogtag, dogtag=dogtag, 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, image_max_width=image_max_width, image_max_height=image_max_height, base64_references=base64_references ) # Legacy edit mode (will be removed) if False: # edit_mode or insert_mode: # Use the original embedded template for edit/insert modes mode_class = 'markitect-edit-mode' if edit_mode else 'markitect-insert-mode' # Convert data to JavaScript-safe strings js_markdown_content = json.dumps(markdown_content) js_markdown_content_with_dogtag = json.dumps(markdown_content_with_dogtag) js_dogtag_content = json.dumps(dogtag) js_base64_references = json.dumps(base64_references or {}) # Get editor configuration version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.5.0.dev" if edit_mode: 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'}' }}; window.editorConfig = MARKITECT_EDITOR_CONFIG;""" else: # insert_mode 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'}' }}; window.editorConfig = MARKITECT_EDITOR_CONFIG;""" # Get editor scripts editor_scripts = self._get_clean_editor_scripts() # Generate CSS css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css # Use the original embedded template structure template_content = """ {{title}} {css_content}
""" # Determine version string for template substitution if version_info: version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" else: version_str = "0.5.0.dev" # Replace template placeholders (same as static mode) html_template = template_content.replace('{title}', title) html_template = html_template.replace('{version}', version_str) # Replace JavaScript variables with properly escaped JSON html_template = html_template.replace('{js_markdown_content}', js_markdown_content) html_template = html_template.replace('{js_markdown_content_with_dogtag}', js_markdown_content_with_dogtag) html_template = html_template.replace('{js_dogtag_content}', js_dogtag_content) html_template = html_template.replace('{js_base64_references}', js_base64_references) html_template = html_template.replace('{editor_config}', editor_config) html_template = html_template.replace('{editor_scripts}', editor_scripts) html_template = html_template.replace('{css_content}', css_content) html_template = html_template.replace('{mode_class}', mode_class) html_template = html_template.replace('{mode_type}', 'insert' if insert_mode else 'edit') # No {content} placeholder in edit mode - content is handled by JavaScript return html_template else: # Use external template for static viewing mode template_path = Path(__file__).parent / 'templates' / 'document.html' if not template_path.exists(): # Fallback to a minimal template if external template not found template_content = """ {title}
{content}
""" else: template_content = template_path.read_text(encoding='utf-8') # Determine version string if version_info: version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" else: version_str = "0.5.0.dev" # Convert markdown to HTML (basic conversion) try: import markdown html_content = markdown.markdown(markdown_content_with_dogtag, extensions=['extra', 'codehilite', 'toc']) except ImportError: # Fallback: simple line breaks and basic formatting html_content = markdown_content_with_dogtag.replace('\n\n', '

').replace('\n', '
') html_content = f'

{html_content}

' # Generate or read CSS content if css: # If css is a file path, read it css_path = Path(css) if css_path.exists() and css_path.is_file(): css_content = f'' else: # Assume it's raw CSS content css_content = f'' else: # Use template-based CSS generation css_content = self._get_template_css(template, image_max_width, image_max_height) # Replace template placeholders using safe string replacement # This avoids conflicts with CSS curly braces html_template = template_content.replace('{title}', title) html_template = html_template.replace('{version}', version_str) html_template = html_template.replace('{content}', html_content) html_template = html_template.replace('{css_content}', css_content) return html_template def _generate_clean_edit_mode_html(self, markdown_content: str, markdown_content_with_dogtag: str, dogtag: 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, image_max_width: str = '12cm', image_max_height: str = '20cm', base64_references: dict = None) -> str: """Generate clean HTML for edit mode using external script references like non-edit mode.""" # Use the fixed template that follows non-edit pattern template_path = Path(__file__).parent / 'templates' / 'edit-mode-fixed.html' if not template_path.exists(): raise FileNotFoundError(f"Fixed edit mode template not found: {template_path}") template_content = template_path.read_text(encoding='utf-8') # Generate or read CSS content if css: # If css is a file path, read it css_path = Path(css) if css_path.exists() and css_path.is_file(): css_content = f'' else: # Assume it's raw CSS content css_content = f'' else: # Use template-based CSS generation css_content = self._get_template_css(template, image_max_width, image_max_height) # Create configuration object - ONLY dynamic data interface config = { 'markdownContent': markdown_content, 'markdownContentWithDogtag': markdown_content_with_dogtag, 'dogtagContent': dogtag, 'mode': 'insert' if insert_mode else 'edit', 'theme': editor_theme, 'keyboardShortcuts': keyboard_shortcuts, 'autosave': False, 'sections': True, 'originalFilename': original_filename, 'base64References': base64_references or {} } # Add version info if version_info: config['version'] = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" config['repoName'] = version_info['repo_name'] else: # Get version from CLI command as fallback import subprocess try: result = subprocess.run(['markitect', '--version'], capture_output=True, text=True, timeout=5) actual_version = result.stdout.strip() if result.returncode == 0 else '0.8.1.dev44+gf788ccdfd.d20251114' except: actual_version = '0.8.1.dev44+gf788ccdfd.d20251114' config['version'] = f'Markitect v{actual_version}' config['repoName'] = 'Markitect' # Add insert mode specific config if insert_mode: config['restrictedHeadingLevels'] = [1, 2, 3] # Convert config to JSON - This is the ONLY place Python data enters JavaScript config_json = json.dumps(config, ensure_ascii=False, separators=(',', ':')) # Mode class for body mode_class = 'markitect-insert-mode' if insert_mode else 'markitect-edit-mode' # Version string for template version_str = config['version'] # Generate fallback content (like non-edit mode) fallback_content = self._render_markdown_to_html(markdown_content_with_dogtag) # Replace template placeholders - all safe static replacements html_template = template_content.replace('{title}', title) html_template = html_template.replace('{version}', version_str) html_template = html_template.replace('{css_content}', css_content) html_template = html_template.replace('{mode_class}', mode_class) html_template = html_template.replace('{config_json}', config_json) html_template = html_template.replace('{fallback_content}', fallback_content) return html_template def _render_markdown_to_html(self, markdown_content: str) -> str: """Render markdown to HTML for fallback content (same as non-edit mode).""" try: from markdown import markdown # Use basic markdown rendering html_content = markdown(markdown_content) # Add target="_blank" to all links (same as non-edit mode) import re html_content = re.sub( r']*)>', r'', html_content ) return html_content except ImportError: # Fallback if markdown not available import html lines = markdown_content.split('\n') html_lines = [] for line in lines: line = line.strip() if line.startswith('# '): html_lines.append(f'

{html.escape(line[2:])}

') elif line.startswith('## '): html_lines.append(f'

{html.escape(line[3:])}

') elif line.startswith('### '): html_lines.append(f'

{html.escape(line[4:])}

') elif line: html_lines.append(f'

{html.escape(line)}

') return '\n'.join(html_lines) def _should_fail_fast(self) -> bool: """ Determine if we should fail fast (development mode) or continue gracefully (production mode). Fail fast in: - Development environments (localhost, 127.0.0.1) - When strict mode is enabled via environment variable - When running in test environments Continue gracefully in: - Production environments - When explicitly disabled via environment variable """ import os # Check environment variables first strict_env = os.getenv('MARKITECT_STRICT_MODE', '').lower() if strict_env in ('true', '1', 'yes', 'on'): return True if strict_env in ('false', '0', 'no', 'off'): return False # Check if we're in a development environment # This mimics the JavaScript strict mode detection try: import socket hostname = socket.gethostname().lower() if 'localhost' in hostname or hostname.startswith('127.') or 'dev' in hostname: return True except: pass # Check for test environment indicators if any(env in os.environ for env in ['PYTEST_CURRENT_TEST', 'CI', 'CONTINUOUS_INTEGRATION', 'TESTING']): return True # Default to graceful handling in production return False def _get_clean_editor_scripts_backup(self) -> str: """Legacy method kept for reference - should not be used.""" # This method contained embedded JavaScript that has been moved to external files return "" def _get_clean_editor_scripts(self) -> str: """Load the modular editor JavaScript components from external files.""" from pathlib import Path # Define the modular components to load in order components = [ 'js/core/debug-system.js', 'js/core/section-manager.js', 'js/components/debug-panel.js', 'js/components/document-controls.js', 'js/components/dom-renderer.js', 'js/controls/control-base.js', 'js/controls/contents-control.js', 'js/controls/status-control.js', 'js/controls/debug-control.js', 'js/controls/edit-control.js', 'js/main.js' ] base_path = Path(__file__).parent / 'static' combined_script = [] # Load each component for component_path in components: script_path = base_path / component_path if script_path.exists(): try: content = script_path.read_text(encoding='utf-8') combined_script.append(f"// === {component_path} ===") combined_script.append(content) combined_script.append("") # Add blank line between components except Exception as e: print(f"Warning: Could not read {component_path}: {e}") combined_script.append(f"console.error('Failed to load {component_path}');") else: print(f"Warning: {component_path} not found at {script_path}") combined_script.append(f"console.error('{component_path} file not found');") # Add initialization script to wire up the components initialization_script = """ // === Missing Function Definitions === function initializeCleanEditor() { console.log('✅ initializeCleanEditor: Modular components will handle initialization'); // This function was missing - the modular components handle initialization automatically // No additional action needed here } function initializeScrollIndicators() { console.log('✅ initializeScrollIndicators: Basic scroll indicators initialized'); // Simple scroll indicator implementation for document navigation // This is a placeholder - can be enhanced later } // === Component Initialization === document.addEventListener('DOMContentLoaded', function() { // Create container for the markdown content const container = document.getElementById('markdown-content') || document.body; // Initialize components const sectionManager = new SectionManager(); const domRenderer = new DOMRenderer(sectionManager, container); const debugPanel = new DebugPanel(); const documentControls = new DocumentControls(); // Create document controls documentControls.create(); // Step 4: Initialize modern Control-based architecture with compass positioning console.log("🎛️ Initializing modern Control system with compass positioning..."); // ContentsControl (positioned upper left - nw) const contentsControl = new ContentsControl(); contentsControl.control.config.position = 'nw'; // Upper left contentsControl.createControl(); window.contentsControl = contentsControl; // StatusControl (positioned right - e) const statusControl = new StatusControl(); statusControl.control.config.position = 'e'; // Right statusControl.createControl(); window.statusControl = statusControl; // DebugControl (positioned lower right - se) const debugControl = new DebugControl(); debugControl.control.config.position = 'se'; // Lower right debugControl.createControl(); window.debugControl = debugControl; // EditControl (positioned upper right - ne) const editControl = new EditControl(); editControl.control.config.position = 'ne'; // Upper right editControl.createControl(); window.editControl = editControl; console.log("🎛️ Modern Control system initialized with compass positioning"); // Wire up event handlers documentControls.setEventHandlers({ 'save-document': () => { console.log('Save document clicked'); try { // Get current markdown content from section manager const currentMarkdown = sectionManager.getDocumentMarkdown(); // Create filename with timestamp suffix following the established convention const now = new Date(); const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-'); // Extract original filename from config or use default const originalFilename = window.editorConfig?.originalFilename || 'document'; const editedFilename = `${originalFilename}-edited-${timestamp}.md`; // Create and download the file const blob = new Blob([currentMarkdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = editedFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Log success to debug panel debugPanel.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS'); console.log(`Document successfully saved as: ${editedFilename}`); } catch (error) { debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR'); console.error('Save error:', error); } }, 'reset-all': () => { console.log('Reset all clicked'); // Hide any open editors domRenderer.hideCurrentEditor(); // Reset all sections to original state const allSections = Array.from(sectionManager.sections.values()); allSections.forEach(section => { section.resetToOriginal(); }); // Re-render all sections domRenderer.renderAllSections(allSections); debugPanel.addMessage(`Reset all sections to original state`, 'INFO'); }, 'show-status': () => { const status = sectionManager.getDocumentStatus(); alert(`Document Status:\\nTotal Sections: ${status.totalSections}\\nEditing Sections: ${status.editingSections}`); }, 'toggle-debug': () => { debugPanel.toggle(); } }); // Set up debug panel integration sectionManager.on('sections-created', (data) => { debugPanel.addMessage(`Created ${data.count} sections`, 'INFO'); }); sectionManager.on('edit-started', (data) => { debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); }); sectionManager.on('changes-accepted', (data) => { debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS'); // Re-render the section to show updated content const section = sectionManager.sections.get(data.sectionId); if (section) { const sectionElement = domRenderer.findSectionElement(data.sectionId); if (sectionElement) { const newElement = domRenderer.renderSection(section); sectionElement.parentNode.replaceChild(newElement, sectionElement); debugPanel.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO'); } } }); sectionManager.on('changes-cancelled', (data) => { debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING'); }); // Initialize with markdown content const markdownToRender = markdownContent || ''; if (markdownToRender.trim()) { const sections = sectionManager.createSectionsFromMarkdown(markdownToRender); domRenderer.renderAllSections(sections); debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO'); } else { debugPanel.addMessage('No markdown content to initialize', 'WARNING'); } // Make components globally available for debugging window.markitectComponents = { sectionManager, domRenderer, debugPanel, documentControls }; console.log('Markitect modular editor initialized successfully'); }); """ combined_script.append(initialization_script) return '\n'.join(combined_script)