diff --git a/cost_notes/issue_132_cost_2025-10-06.md b/cost_notes/issue_132_cost_2025-10-06.md new file mode 100644 index 00000000..dfe87a42 --- /dev/null +++ b/cost_notes/issue_132_cost_2025-10-06.md @@ -0,0 +1,73 @@ +--- +note_type: "issue_cost_tracking" +issue_id: 132 +issue_title: "Instant Markdown JavaScript client-side rendering with dark theme" +session_date: "2025-10-06" +claude_model: "claude-sonnet-4" +total_cost_eur: 0.1725 +total_cost_usd: 0.1875 +total_tokens: 24500 +generated_at: "2025-10-06T23:39:38.084720" +--- + +# Issue #132 Implementation Cost +**Issue**: Instant Markdown JavaScript client-side rendering with dark theme +**Date**: 2025-10-06 +**Claude Model**: claude-sonnet-4 + +## Cost Summary +- **Total Cost**: €0.1725 ($0.1875 USD) +- **Token Usage**: 24,500 tokens +- **Input Tokens**: 15,000 tokens @ $3.00/M +- **Output Tokens**: 9,500 tokens @ $15.00/M + +## Cost Breakdown + +| Component | Tokens | Rate ($/M) | Cost (USD) | Cost (EUR) | +|-----------|--------|------------|------------|------------| +| Input | 15,000 | $3.00 | $0.0450 | €0.0414 | +| Output | 9,500 | $15.00 | $0.1425 | €0.1311 | +| **Total** | 24,500 | - | $0.1875 | €0.1725 | + +## Implementation Summary +Implemented comprehensive TDD8 workflow for client-side markdown rendering. Added md-render command with 4 templates (basic, github, academic, dark), custom CSS injection, YAML front matter support, and self-contained HTML output. Complete feature with 11+ tests passing. + +## Cost Allocation +This cost has been allocated to the 'AI & ML Services' category as a one-time expense for issue #132 implementation. + +## Notes +- Currency conversion rate: 1 USD = 0.920 EUR +- Pricing based on claude-sonnet-4 rates as of 2025-10-06 +- Token counts and costs are estimates based on session usage + + \ No newline at end of file diff --git a/markitect/plugins/builtin/markdown_commands.py b/markitect/plugins/builtin/markdown_commands.py index 45ceb490..6a3a1a1a 100644 --- a/markitect/plugins/builtin/markdown_commands.py +++ b/markitect/plugins/builtin/markdown_commands.py @@ -6,6 +6,8 @@ replacing the legacy unprefixed commands for better namespace consistency. """ import click +import json +import tempfile from pathlib import Path from typing import Dict, Any @@ -39,7 +41,8 @@ class MarkdownCommandsPlugin(CommandPlugin): return { 'md-ingest': md_ingest_command, 'md-get': md_get_command, - 'md-list': md_list_command + 'md-list': md_list_command, + 'md-render': md_render_command } @@ -237,4 +240,258 @@ def md_list_command(ctx, output_format, names_only): except Exception as e: click.echo(f"Error listing files: {e}", err=True) - raise click.Abort() \ No newline at end of file + 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.pass_context +def md_render_command(ctx, input_file, output, template, css): + """ + 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 + • 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 + + # 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 + ) + + # 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): + """Generate HTML with embedded markdown content for client-side rendering.""" + + # Get template styles or default to basic + styles = TEMPLATE_STYLES.get(template, TEMPLATE_STYLES['basic']) + + # HTML template with style variables + html_template = ''' + +
+ + +