""" 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.""" # 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 = "" # Pass original markdown content to editor (without dogtag for editing) # But make dogtag available separately for protected display in editor 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 {}) # 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, image_max_width, image_max_height) # 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() else: editor_scripts = "" # Generate the complete HTML template html_template = f"""
${this.config.defaultContent}
`; }, // Concrete methods (shared by all controls) createControl: function() { console.log(`🎛️ Creating ${this.config.title} control...`); this.element = document.createElement('div'); this.element.className = this.config.className; this.element.innerHTML = ` `; // Position using compass direction const positionStyles = this.getPositionStyles(); this.element.style.cssText = ` position: ${positionStyles.position}; top: ${positionStyles.top}; right: ${positionStyles.right}; bottom: ${positionStyles.bottom}; left: ${positionStyles.left}; transform: ${positionStyles.transform}; z-index: ${positionStyles.zIndex}; background: rgba(255, 255, 255, 0.95); border: 1px solid #e1e5e9; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); backdrop-filter: blur(8px); width: 40px; transition: all 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; `; // Store original position for reset this.originalPosition = { top: positionStyles.top, right: positionStyles.right, bottom: positionStyles.bottom, left: positionStyles.left, transform: positionStyles.transform }; // Style toggle button const toggleBtn = this.element.querySelector('.control-toggle'); toggleBtn.style.cssText = ` width: 100%; height: 40px; border: none; background: transparent; cursor: pointer; font-size: 16px; color: #666; transition: color 0.2s ease; `; // Handle click to build content on-demand toggleBtn.addEventListener('click', () => { if (this.isExpanded) { this.collapse(); } else { console.log(`🎛️ ${this.config.title} toggle clicked - building content...`); this.buildContent(); } }); // Close button handler const closeBtn = this.element.querySelector('.control-close'); closeBtn.addEventListener('click', () => { this.collapse(); }); document.body.appendChild(this.element); console.log(`🎛️ ${this.config.title} control created`); }, styleHeader: function() { const header = this.element.querySelector('.control-header'); // Style the header to show icon, title, and close button in one line // Match the height of the collapsed icon state (40px) header.style.cssText = ` display: flex; align-items: center; justify-content: space-between; height: 40px; padding: 0 1rem; border-bottom: 1px solid #eee; margin-bottom: 0; `; const icon = header.querySelector('.control-icon'); if (icon) { icon.style.cssText = ` font-size: 16px; color: #666; margin-right: 0.5rem; cursor: grab; user-select: none; `; // Make icon draggable this.setupDragHandlers(icon); } const title = header.querySelector('h3'); if (title) { title.style.cssText = ` margin: 0; font-size: 0.9rem; font-weight: 600; flex-grow: 1; line-height: 1; `; } const closeBtn = header.querySelector('.control-close'); if (closeBtn) { closeBtn.style.cssText = ` background: none; border: none; font-size: 14px; cursor: pointer; color: #666; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; `; } }, styleContent: function() { const content = this.element.querySelector('.control-content'); const expansion = this.getExpansionDirection(); // Style the content area based on expansion direction let contentStyles = ` padding: 0.5rem; overflow-y: auto; `; if (expansion.body === 'up') { // Body expands upward (for bottom border positions) contentStyles += ` max-height: calc(80vh - 40px); `; content.parentElement.style.flexDirection = 'column-reverse'; } else { // Body expands downward (default) contentStyles += ` max-height: calc(80vh - 40px); `; content.parentElement.style.flexDirection = 'column'; } content.style.cssText = contentStyles; }, expand: function() { this.isExpanded = true; const panel = this.element.querySelector('.control-panel'); const toggleBtn = this.element.querySelector('.control-toggle'); // Get expansion direction based on compass position const expansion = this.getExpansionDirection(); // Apply expansion styling based on direction if (expansion.header === 'left') { // Header expands to the left (for right border positions) this.element.style.width = '300px'; this.element.style.transformOrigin = 'top right'; } else { // Header expands to the right (default) this.element.style.width = '300px'; this.element.style.transformOrigin = 'top left'; } panel.style.display = 'block'; toggleBtn.style.display = 'none'; this.styleHeader(); this.styleContent(); }, collapse: function() { this.isExpanded = false; const panel = this.element.querySelector('.control-panel'); const toggleBtn = this.element.querySelector('.control-toggle'); panel.style.display = 'none'; this.element.style.width = '40px'; toggleBtn.style.display = 'block'; // Reset position to original compass location this.element.style.top = this.originalPosition.top; this.element.style.right = this.originalPosition.right; this.element.style.bottom = this.originalPosition.bottom; this.element.style.left = this.originalPosition.left; this.element.style.transform = this.originalPosition.transform; }, setupDragHandlers: function(dragElement) { dragElement.addEventListener('mousedown', (e) => { this.isDragging = true; const rect = this.element.getBoundingClientRect(); const iconRect = dragElement.getBoundingClientRect(); // Calculate offset relative to the icon position, not the element this.dragOffset.x = e.clientX - rect.left; this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center dragElement.style.cursor = 'grabbing'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!this.isDragging || !this.isExpanded) return; const newX = e.clientX - this.dragOffset.x; const newY = e.clientY - this.dragOffset.y; // Keep within viewport bounds const maxX = window.innerWidth - this.element.offsetWidth; const maxY = window.innerHeight - this.element.offsetHeight; const boundedX = Math.max(0, Math.min(newX, maxX)); const boundedY = Math.max(0, Math.min(newY, maxY)); this.element.style.left = boundedX + 'px'; this.element.style.top = boundedY + 'px'; }); document.addEventListener('mouseup', () => { if (this.isDragging) { this.isDragging = false; dragElement.style.cursor = 'grab'; } }); } }; // Create ContentsControl for edit mode (new implementation based on Control class) try { const contentsControl = Object.create(Control); // Configure for contents navigation in edit mode contentsControl.config = { icon: '☰', title: 'Contents', className: 'contents-control edit-mode', defaultContent: 'No headings found', ariaLabel: 'Document Navigation', position: 'wnw' // West-north-west positioning }; // Override buildContent method for navigation functionality contentsControl.buildContent = function() { const content = this.element.querySelector('.control-content'); // Build navigation content from current DOM const allHeadings = document.querySelectorAll('h1, h2, h3'); // Filter out headings that contain "Contents" or similar navigation-related text const headings = Array.from(allHeadings).filter(heading => { const text = heading.textContent.trim().toLowerCase(); return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation'); }); console.log("📋 Found headings for navigation:", headings.length); if (headings.length === 0) { content.innerHTML = 'No headings found
'; } else { let navHtml = ''; headings.forEach((heading, index) => { if (!heading.id) { heading.id = `heading-${index + 1}`; } const level = parseInt(heading.tagName.substring(1)); const indent = (level - 1) * 1; navHtml += ` ${heading.textContent.trim()} `; }); content.innerHTML = navHtml; } // Show panel this.expand(); }; // Initialize the ContentsControl contentsControl.createControl(); // Make globally available for mobile collapse window.contentsControl = contentsControl; } catch (error) { console.error("ContentsControl failed to initialize:", error); } // 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)