""" Markdown commands plugin for MarkiTect. This plugin provides the core markdown file operations with md- prefixes, replacing the legacy unprefixed commands for better namespace consistency. """ import click import json import tempfile from pathlib import Path from typing import Dict, Any from markitect.plugins.base import CommandPlugin, PluginMetadata, PluginType from markitect.plugins.decorators import register_plugin from markitect.document_manager import DocumentManager from markitect.serializer import ASTSerializer # Simple helper function - avoiding circular imports def get_default_format(available_formats=['table', 'json', 'yaml', 'simple'], fallback='simple'): """Get the default output format - simplified version for plugin.""" return fallback @register_plugin("markdown_commands") class MarkdownCommandsPlugin(CommandPlugin): """Plugin providing core markdown file operations.""" @property def metadata(self) -> PluginMetadata: return PluginMetadata( name="markdown_commands", version="1.0.0", description="Core markdown file operations (ingest, get, list) with md- prefixes", author="MarkiTect Core Team", plugin_type=PluginType.COMMAND, markitect_version=">=0.1.0" ) def get_commands(self) -> Dict[str, Any]: """Return the markdown commands with md- prefixes.""" return { 'md-ingest': md_ingest_command, 'md-get': md_get_command, 'md-list': md_list_command, 'md-render': md_render_command } # Define commands as standalone functions @click.command() @click.argument('file_path', type=click.Path(exists=True)) @click.pass_context def md_ingest_command(ctx, file_path): """ Process and store a markdown file. Ingests a markdown file into the MarkiTect system, parsing its content, extracting front matter, generating AST cache, and storing metadata in the database. FILE_PATH: Path to the markdown file to process Examples: markitect md-ingest README.md markitect md-ingest docs/guide.md """ config = ctx.obj or {} try: if config.get('verbose', False): click.echo(f"Processing file: {file_path}") # Initialize document manager with database manager doc_manager = DocumentManager(config.get('db_manager')) # Process the file result = doc_manager.ingest_file(file_path) if config.get('verbose', False): click.echo(f"Processing results:") click.echo(f" File: {result['metadata']['filename']}") click.echo(f" AST nodes: {len(result['ast'])} nodes") click.echo(f" Cache file: {result['ast_cache_path']}") click.echo(f" Parse time: {result['parse_time']:.2f}s") click.echo(f" Cache time: {result['cache_time']:.2f}s") click.echo(f"✓ Successfully ingested: {Path(file_path).name}") except Exception as e: click.echo(f"Error processing file: {e}", err=True) raise click.Abort() @click.command() @click.argument('file_path', type=str) @click.option('--output', '-o', type=click.Path(), help='Output file path (default: stdout)') @click.pass_context def md_get_command(ctx, file_path, output): """ Retrieve and output a processed markdown file. Loads the file from the database and AST cache, then serializes it back to markdown format. Supports outputting to file or stdout. FILE_PATH: Name of the file to retrieve Examples: markitect md-get README.md markitect md-get docs/guide.md --output modified_guide.md """ config = ctx.obj or {} try: if config.get('verbose', False): click.echo(f"Retrieving file: {file_path}") db_manager = config.get('db_manager') # Get file information from database file_info = db_manager.get_markdown_file(file_path) if not file_info: click.echo(f"File not found in database: {file_path}", err=True) click.echo("Use 'markitect md-ingest' to process the file first.", err=True) raise click.Abort() # Load AST from cache cache_filename = f"{file_path}.ast.json" cache_path = Path('.ast_cache') / cache_filename if not cache_path.exists(): click.echo(f"AST cache not found: {cache_path}", err=True) click.echo("Try re-ingesting the file to regenerate cache.", err=True) raise click.Abort() # Read AST from cache import json with open(cache_path, 'r', encoding='utf-8') as f: ast = json.load(f) # Parse front matter from database front_matter = None if file_info.get('front_matter'): try: front_matter = eval(file_info['front_matter']) except (ValueError, TypeError, SyntaxError): if config.get('verbose', False): click.echo("Warning: Could not parse front matter", err=True) # Serialize AST back to markdown serializer = ASTSerializer() markdown_content = serializer.serialize_to_markdown(ast, front_matter) # Output to file or stdout if output: output_path = Path(output) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(markdown_content) click.echo(f"✓ File written to: {output_path}") else: click.echo(markdown_content) if config.get('verbose', False): click.echo(f"Retrieved {len(ast)} AST tokens", err=True) except Exception as e: click.echo(f"Error retrieving file: {e}", err=True) raise click.Abort() @click.command() @click.option('--format', 'output_format', type=click.Choice(['table', 'json', 'yaml', 'simple']), default=lambda: get_default_format(['table', 'json', 'yaml', 'simple']), help='Output format') @click.option('--names-only', is_flag=True, help='Show only filenames (no metadata)') @click.pass_context def md_list_command(ctx, output_format, names_only): """ List all stored markdown files and their status. Shows all markdown files that have been processed and stored in the MarkiTect database with their basic metadata. Examples: markitect md-list markitect md-list --format table markitect md-list --format json markitect md-list --names-only """ config = ctx.obj or {} try: if config.get('verbose', False): click.echo("Retrieving all stored files...") db_manager = config.get('db_manager') files = db_manager.list_markdown_files() if not files: click.echo("No files found in database.") click.echo("Use 'markitect md-ingest ' to add files.") return # Handle names-only option if names_only: for file_info in files: click.echo(file_info['filename']) return # Handle different output formats if output_format == 'simple': # Original emoji format click.echo(f"Found {len(files)} file(s):") click.echo() for file_info in files: click.echo(f"📄 {file_info['filename']}") if config.get('verbose', False): click.echo(f" Created: {file_info['created_at']}") if file_info.get('front_matter'): try: front_matter = eval(file_info['front_matter']) if front_matter: click.echo(f" Front matter: {list(front_matter.keys())}") except (ValueError, TypeError, SyntaxError): click.echo(f" Front matter: (parsing error)") click.echo() else: # Use structured format (table, json, yaml) if output_format == 'json': import json click.echo(json.dumps(files, indent=2, default=str)) elif output_format == 'yaml': import yaml click.echo(yaml.dump(files, default_flow_style=False)) else: # table format (default) # Simple table output click.echo(f"Found {len(files)} file(s):") click.echo(f"{'Filename':<30} {'Created':<20}") click.echo("-" * 50) for file_info in files: click.echo(f"{file_info['filename']:<30} {file_info['created_at']:<20}") except Exception as e: click.echo(f"Error listing files: {e}", err=True) raise click.Abort() @click.command() @click.argument('input_file', type=click.Path(exists=True)) @click.option('--output', '-o', type=click.Path(), help='Output HTML file path (defaults to input filename with .html extension)') @click.option('--template', type=click.Choice(['basic', 'github', 'academic', 'dark']), default='basic', help='HTML template: basic (default), github, academic, or dark theme') @click.option('--css', type=click.Path(exists=True), help='Custom CSS file to inject into the template') @click.option('--edit', is_flag=True, help='Enable instant markdown editing capabilities in the generated HTML') @click.option('--editor-theme', type=click.Choice(['light', 'dark']), default='light', help='Editor interface theme (light or dark)') @click.option('--keyboard-shortcuts', is_flag=True, help='Enable keyboard shortcuts for editing actions') @click.pass_context def md_render_command(ctx, input_file, output, template, css, edit, editor_theme, keyboard_shortcuts): """ Generate HTML with client-side JavaScript markdown rendering. Creates a self-contained HTML file that includes the markdown content as JavaScript data and renders it in the browser using client-side markdown parsing with marked.js. The generated HTML includes: • Embedded markdown content as JavaScript payload • Client-side rendering with marked.js from CDN • YAML front matter support and metadata extraction • Multiple responsive template options • Custom CSS injection capability • Optional instant editing capabilities with --edit flag • Graceful fallback if JavaScript fails INPUT_FILE: Path to the markdown file to render Available Templates: • basic (default) - Clean, minimal design with system fonts • github - GitHub-style appearance with heading underlines • academic - Academic paper style with serif fonts and justified text • dark - GitHub dark mode inspired theme with dark background Examples: # Basic usage with default template markitect md-render README.md # Specify output file and template markitect md-render README.md --output index.html --template github # Dark theme for night reading markitect md-render docs/guide.md --template dark # Academic paper with custom styling markitect md-render paper.md --template academic --css custom.css # Enable instant editing capabilities markitect md-render README.md --edit # Editing with dark editor theme and keyboard shortcuts markitect md-render docs/guide.md --edit --editor-theme dark --keyboard-shortcuts # Front matter will be parsed and available to JavaScript # Files with YAML front matter are fully supported """ config = ctx.obj or {} try: if config.get('verbose', False): click.echo(f"Rendering file: {input_file}") # Read markdown file input_path = Path(input_file) markdown_content = input_path.read_text(encoding='utf-8') # Extract front matter if present front_matter = {} if markdown_content.startswith('---\n'): parts = markdown_content.split('---\n', 2) if len(parts) >= 3: try: import yaml front_matter = yaml.safe_load(parts[1]) or {} markdown_content = parts[2] except ImportError: # Fallback without yaml parsing pass # Generate title from first heading or filename title = front_matter.get('title', input_path.stem) lines = markdown_content.strip().split('\n') for line in lines: if line.startswith('# '): title = line[2:].strip() break # Load custom CSS if provided css_content = "" if css: css_path = Path(css) css_content = css_path.read_text(encoding='utf-8') # Generate HTML with embedded markdown html_content = generate_html_with_embedded_markdown( markdown_content, title, template, css_content, front_matter, edit, editor_theme, keyboard_shortcuts ) # Determine output path if not output: output = input_path.with_suffix('.html') else: output = Path(output) # Ensure output directory exists output.parent.mkdir(parents=True, exist_ok=True) # Write HTML file output.write_text(html_content, encoding='utf-8') click.echo(f"✓ HTML generated: {output}") if config.get('verbose', False): click.echo(f" Template: {template}") click.echo(f" Title: {title}") if css: click.echo(f" Custom CSS: {css}") except Exception as e: click.echo(f"Error rendering file: {e}", err=True) raise click.Abort() # Template definitions for cleaner code organization TEMPLATE_STYLES = { 'basic': { 'body_color': '#333', 'body_bg': '', 'heading_color': '#2c3e50', 'heading_border': '', 'code_bg': '#f4f4f4', 'code_border': '', 'blockquote_border': '#ddd', 'blockquote_color': '#666', 'font_family': '-apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Roboto\', \'Helvetica\', \'Arial\', sans-serif', 'max_width': '800px', 'text_align': '' }, 'github': { 'body_color': '#24292e', 'body_bg': 'background-color: #ffffff;', 'heading_color': '#1f2328', 'heading_border': 'border-bottom: 1px solid #d0d7de; padding-bottom: 0.3em;', 'code_bg': '#f4f4f4', 'code_border': '', 'blockquote_border': '#ddd', 'blockquote_color': '#666', 'font_family': '-apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Roboto\', \'Helvetica\', \'Arial\', sans-serif', 'max_width': '800px', 'text_align': '' }, 'academic': { 'body_color': '#333', 'body_bg': '', 'heading_color': '#2c3e50', 'heading_border': '', 'code_bg': '#f4f4f4', 'code_border': '', 'blockquote_border': '#ddd', 'blockquote_color': '#666', 'font_family': '"Times New Roman", Times, serif', 'max_width': '900px', 'text_align': 'text-align: justify;' }, 'dark': { 'body_color': '#e1e4e8', 'body_bg': 'background-color: #0d1117;', 'heading_color': '#58a6ff', 'heading_border': 'border-bottom: 1px solid #21262d; padding-bottom: 0.3em;', 'code_bg': '#161b22', 'code_border': 'border: 1px solid #21262d;', 'blockquote_border': '#58a6ff', 'blockquote_color': '#8b949e', 'font_family': '-apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Roboto\', \'Helvetica\', \'Arial\', sans-serif', 'max_width': '800px', 'text_align': '' } } def generate_html_with_embedded_markdown(markdown_content, title, template, css_content, front_matter, edit=False, editor_theme='light', keyboard_shortcuts=False): """Generate HTML with embedded markdown content for client-side rendering. Args: markdown_content: The markdown content to embed title: Page title template: Template name (basic, github, academic, dark) css_content: Custom CSS content to inject front_matter: YAML front matter dictionary edit: Enable editing capabilities editor_theme: Editor theme (light or dark) keyboard_shortcuts: Enable keyboard shortcuts """ # Get template styles or default to basic styles = TEMPLATE_STYLES.get(template, TEMPLATE_STYLES['basic']) # Build editor styles if editing is enabled editor_styles = "" if edit: editor_styles = ''' /* Markitect Editor Styles */ .markitect-floating-header {{ position: fixed; top: 10px; right: 10px; background: rgba(0, 123, 255, 0.9); color: white; padding: 10px 20px; border-radius: 20px; font-size: 14px; font-weight: bold; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 1000; display: none; }} .markitect-floating-header.show {{ display: block; }} .markitect-section-editable {{ position: relative; cursor: pointer; transition: background-color 0.2s; }} .markitect-section-editable:hover {{ background-color: rgba(0, 123, 255, 0.1); }} .markitect-section-modified {{ border-left: 4px solid #007bff; padding-left: 16px; }} .markitect-edit-interface {{ margin: 15px 0; padding: 20px; border: 2px dashed #007bff; border-radius: 8px; background: #f8f9fa; }} .markitect-edit-textarea {{ width: 100%; min-height: 150px; font-family: 'Courier New', Consolas, monospace; font-size: 14px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; }} .markitect-edit-actions {{ margin-top: 10px; text-align: right; }} .markitect-edit-btn {{ margin-left: 10px; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }} .markitect-btn-apply {{ background-color: #28a745; color: white; }} .markitect-btn-reset {{ background-color: #ffc107; color: #212529; }} .markitect-btn-cancel {{ background-color: #6c757d; color: white; }} .markitect-btn-save {{ background-color: #007bff; color: white; padding: 10px 20px; margin-left: 15px; }} ''' if editor_theme == 'dark': editor_styles += ''' /* Dark theme overrides */ .markitect-edit-interface {{ background: #2d2d2d; border-color: #666; }} .markitect-edit-textarea {{ background: #1a1a1a; color: #f0f0f0; border-color: #666; }} ''' # HTML template with style variables html_template = ''' {title}
{editor_html} {editor_scripts} ''' # Build editor HTML components if editing is enabled editor_html = "" editor_scripts = "" editor_config = "" if edit: editor_config = ''' // Editor configuration window.MARKITECT_EDIT_MODE = true; window.MARKITECT_EDITOR_CONFIG = { theme: \'''' + editor_theme + '''\', keyboardShortcuts: ''' + ('true' if keyboard_shortcuts else 'false') + ''' };''' editor_html = '''
0 sections changed
''' # Basic JavaScript editor implementation editor_scripts = ''' ''' # Format template with styles and content return html_template.format( title=title, css_content=css_content, editor_styles=editor_styles, editor_html=editor_html, editor_scripts=editor_scripts, editor_config=editor_config, markdown_json=json.dumps(markdown_content), front_matter_json=json.dumps(front_matter), **styles )