#!/usr/bin/env nu # Markdown processing module with frontmatter support # Supports multiple processors with Rust tools as default # Main Markdown processing function export def process_markdown [markdown_file: string, config: record, dev: bool, verbose: bool] { let markdown_config = if "markdown" in ($config | columns) { $config.markdown } else { { enabled: false } } if not $markdown_config.enabled { if $verbose { print "📝 Markdown processing disabled, skipping..." } return { html: "", metadata: {}, size: 0, success: false } } if not ($markdown_file | path exists) { if $verbose { print $"❌ Markdown file not found: ($markdown_file)" } return { html: "", metadata: {}, size: 0, success: false } } if $verbose { print "📝 Processing Markdown file..." } # Read and parse Markdown file with frontmatter let parsed_content = (parse_markdown_with_frontmatter $markdown_file $markdown_config $verbose) if not $parsed_content.success { return { html: "", metadata: {}, size: 0, success: false } } # Process Markdown to HTML let html_result = (markdown_to_html $parsed_content.content $markdown_config $verbose) if not $html_result.success { return { html: "", metadata: $parsed_content.metadata, size: 0, success: false } } # Apply HTML template if specified let final_html = (apply_markdown_template $html_result.html $parsed_content.metadata $config $verbose) { html: $final_html, metadata: $parsed_content.metadata, size: ($final_html | str length), success: true } } # Parse Markdown file with frontmatter (YAML or TOML) def parse_markdown_with_frontmatter [file_path: string, markdown_config: record, verbose: bool] { let content = (open $file_path) let frontmatter_format = if "frontmatter_format" in ($markdown_config | columns) { $markdown_config.frontmatter_format } else { "yaml" } mut metadata = {} mut markdown_content = $content # Check for frontmatter delimiters if ($content | str starts-with "---") { # YAML frontmatter let parts = ($content | str replace "---" "FRONTMATTER_DELIMITER" | split row "FRONTMATTER_DELIMITER") if ($parts | length) >= 3 { let frontmatter_raw = ($parts | get 1 | str trim) $markdown_content = ($parts | skip 2 | str join "FRONTMATTER_DELIMITER" | str trim) if ($frontmatter_raw | str length) > 0 { $metadata = (parse_frontmatter $frontmatter_raw "yaml" $verbose) } } } else if ($content | str starts-with "+++") { # TOML frontmatter let parts = ($content | str replace "+++" "FRONTMATTER_DELIMITER" | split row "FRONTMATTER_DELIMITER") if ($parts | length) >= 3 { let frontmatter_raw = ($parts | get 1 | str trim) $markdown_content = ($parts | skip 2 | str join "FRONTMATTER_DELIMITER" | str trim) if ($frontmatter_raw | str length) > 0 { $metadata = (parse_frontmatter $frontmatter_raw "toml" $verbose) } } } if $verbose and (($metadata | columns | length) > 0) { print $" 📋 Parsed frontmatter: ($metadata | columns | str join ', ')" } { content: $markdown_content, metadata: $metadata, success: true } } # Parse frontmatter content (YAML or TOML) def parse_frontmatter [content: string, format: string, verbose: bool] { try { match $format { "yaml" => { # For now, we'll use a simple YAML parser # In a real implementation, we'd use a proper YAML library parse_simple_yaml $content $verbose } "toml" => { # Parse TOML frontmatter $content | from toml } _ => { if $verbose { print $" ⚠️ Unknown frontmatter format: ($format)" } {} } } } catch { |e| if $verbose { print $" ⚠️ Failed to parse frontmatter: ($e.msg)" } {} } } # Simple YAML parser for basic frontmatter def parse_simple_yaml [content: string, verbose: bool] { mut result = {} let lines = ($content | lines | where { |line| ($line | str trim | str length) > 0 and not ($line | str starts-with "#") }) for line in $lines { if ($line | str contains ":") { let parts = ($line | split row ":" | take 2) if ($parts | length) == 2 { let key = ($parts | get 0 | str trim) let value_raw = ($parts | get 1 | str trim) # Parse different value types let value = if ($value_raw | str starts-with '"') and ($value_raw | str ends-with '"') { # String value $value_raw | str replace '"' '' | str replace '"' '' } else if ($value_raw | str starts-with '[') and ($value_raw | str ends-with ']') { # Array value (simple) $value_raw | str replace '[' '' | str replace ']' '' | split row ',' | each { |item| $item | str trim | str replace '"' '' } } else if $value_raw in ["true", "false"] { # Boolean value $value_raw == "true" } else { # Try to parse as number, fallback to string try { $value_raw | into int } catch { try { $value_raw | into float } catch { $value_raw } } } $result = ($result | insert $key $value) } } } $result } # Convert Markdown to HTML using configured processor def markdown_to_html [markdown_content: string, markdown_config: record, verbose: bool] { let processor = if "processor" in ($markdown_config | columns) { $markdown_config.processor } else { "pulldown-cmark" # Rust default } if $verbose { print $" 🔧 Using Markdown processor: ($processor)" } match $processor { "pulldown-cmark" => { rust_pulldown_cmark $markdown_content $markdown_config $verbose } "comrak" => { rust_comrak $markdown_content $markdown_config $verbose } "pandoc" => { external_pandoc $markdown_content $markdown_config $verbose } "marked" => { external_marked $markdown_content $markdown_config $verbose } "markdown-it" => { external_markdown_it $markdown_content $markdown_config $verbose } _ => { if $verbose { print $" ⚠️ Unknown processor ($processor), falling back to pulldown-cmark" } rust_pulldown_cmark $markdown_content $markdown_config $verbose } } } # Process Markdown using Rust pulldown-cmark (default) def rust_pulldown_cmark [content: string, config: record, verbose: bool] { # Check if pulldown-cmark is available as a CLI tool let has_pulldown_cmark = (which pulldown-cmark | length) > 0 if $has_pulldown_cmark { try { # Create temp file for input let temp_input = $"/tmp/markdown_input_(date now | format date '%s').md" $content | save $temp_input # Process with pulldown-cmark let html_output = (run-external "pulldown-cmark" $temp_input) # Clean up rm $temp_input { html: $html_output, success: true } } catch { |e| if $verbose { print $" ❌ pulldown-cmark failed: ($e.msg)" } # Fallback to built-in simple processor simple_markdown_processor $content $config $verbose } } else { if $verbose { print " 💡 pulldown-cmark not found, using built-in processor" } simple_markdown_processor $content $config $verbose } } # Process Markdown using Rust comrak def rust_comrak [content: string, config: record, verbose: bool] { let has_comrak = (which comrak | length) > 0 if $has_comrak { try { # Create temp file for input let temp_input = $"/tmp/markdown_input_(date now | format date '%s').md" $content | save $temp_input # Process with comrak (GitHub-flavored Markdown) let args = ["--gfm", "--unsafe", $temp_input] let html_output = (run-external "comrak" ...$args) # Clean up rm $temp_input { html: $html_output, success: true } } catch { |e| if $verbose { print $" ❌ comrak failed: ($e.msg)" } # Fallback to built-in processor simple_markdown_processor $content $config $verbose } } else { if $verbose { print " 💡 comrak not found, falling back to pulldown-cmark" } rust_pulldown_cmark $content $config $verbose } } # Process Markdown using external pandoc def external_pandoc [content: string, config: record, verbose: bool] { let has_pandoc = (which pandoc | length) > 0 if $has_pandoc { try { # Create temp file for input let temp_input = $"/tmp/markdown_input_(date now | format date '%s').md" $content | save $temp_input # Process with pandoc let args = ["-f", "markdown", "-t", "html", $temp_input] let html_output = (run-external "pandoc" ...$args) # Clean up rm $temp_input { html: $html_output, success: true } } catch { |e| if $verbose { print $" ❌ pandoc failed: ($e.msg)" } # Fallback to Rust processor rust_pulldown_cmark $content $config $verbose } } else { if $verbose { print " 💡 pandoc not found, falling back to pulldown-cmark" } rust_pulldown_cmark $content $config $verbose } } # Process Markdown using external marked (Node.js) def external_marked [content: string, config: record, verbose: bool] { let has_marked = (which marked | length) > 0 if $has_marked { try { # Create temp file for input let temp_input = $"/tmp/markdown_input_(date now | format date '%s').md" $content | save $temp_input # Process with marked let args = [$temp_input] let html_output = (run-external "marked" ...$args) # Clean up rm $temp_input { html: $html_output, success: true } } catch { |e| if $verbose { print $" ❌ marked failed: ($e.msg)" } # Fallback to Rust processor rust_pulldown_cmark $content $config $verbose } } else { if $verbose { print " 💡 marked not found, falling back to pulldown-cmark" } rust_pulldown_cmark $content $config $verbose } } # Process Markdown using external markdown-it (Node.js) def external_markdown_it [content: string, config: record, verbose: bool] { let has_markdown_it = (which markdown-it | length) > 0 if $has_markdown_it { try { # Create temp file for input let temp_input = $"/tmp/markdown_input_(date now | format date '%s').md" $content | save $temp_input # Process with markdown-it let args = [$temp_input] let html_output = (run-external "markdown-it" ...$args) # Clean up rm $temp_input { html: $html_output, success: true } } catch { |e| if $verbose { print $" ❌ markdown-it failed: ($e.msg)" } # Fallback to Rust processor rust_pulldown_cmark $content $config $verbose } } else { if $verbose { print " 💡 markdown-it not found, falling back to pulldown-cmark" } rust_pulldown_cmark $content $config $verbose } } # Simple built-in Markdown processor (fallback) def simple_markdown_processor [content: string, config: record, verbose: bool] { if $verbose { print " 🔧 Using built-in Markdown processor" } mut html = $content # Headers $html = ($html | str replace -ar '^# (.+)$' '
$2')
$html = ($html | str replace -ar '`([^`]+)`' '$1')
# Links
$html = ($html | str replace -ar '\[([^\]]+)\]\(([^)]+)\)' '$1')
# Paragraphs (simple)
let lines = ($html | lines)
let processed_lines = ($lines | each { |line|
if ($line | str trim | str length) == 0 {
""
} else if ($line | str starts-with "<") {
$line
} else {
$"($line)
" } }) $html = ($processed_lines | str join "\n") { html: $html, success: true } } # Apply HTML template to Markdown content def apply_markdown_template [html_content: string, metadata: record, config: record, verbose: bool] { let markdown_config = if "markdown" in ($config | columns) { $config.markdown } else { {} } let template_name = if "template" in ($metadata | columns) { $metadata.template } else if "template" in ($markdown_config | columns) { $markdown_config.template } else { "article" } if $verbose { print $" 🎨 Applying template: ($template_name)" } # Load template let template_result = (load_markdown_template $template_name $config $verbose) if not $template_result.success { if $verbose { print " ⚠️ Template loading failed, using content as-is" } return $html_content } # Replace template variables let final_html = (render_template $template_result.content $html_content $metadata $config) $final_html } # Load HTML template for Markdown content def load_markdown_template [template_name: string, config: record, verbose: bool] { # Find framework root let current_dir = $env.PWD let framework_templates = ($current_dir | path join "framework" "templates" "markdown") let template_path = ($framework_templates | path join $"($template_name).html") if ($template_path | path exists) { let content = (open $template_path) { content: $content, success: true } } else { # Try custom template path if specified let markdown_config = if "markdown" in ($config | columns) { $config.markdown } else { {} } let custom_template = if "custom_template" in ($markdown_config | columns) { $markdown_config.custom_template } else { "" } if ($custom_template | str length) > 0 and ($custom_template | path exists) { let content = (open $custom_template) { content: $content, success: true } } else { if $verbose { print $" ❌ Template not found: ($template_path)" } { content: "", success: false } } } } # Render template with variables def render_template [template: string, content: string, metadata: record, config: record] { mut rendered = $template # Replace content placeholder $rendered = ($rendered | str replace "{{CONTENT}}" $content) # Replace metadata variables for key in ($metadata | columns) { let value = $metadata | get $key let placeholder = $"{{($key | str upcase)}}" $rendered = ($rendered | str replace $placeholder ($value | into string)) } # Replace config variables let page_config = if "page" in ($config | columns) { $config.page } else { {} } for key in ($page_config | columns) { let value = $page_config | get $key let placeholder = $"{{PAGE_($key | str upcase)}}" $rendered = ($rendered | str replace $placeholder ($value | into string)) } # Default fallbacks $rendered = ($rendered | str replace -ar '{{[^}]+}}' '') # Remove unmatched placeholders $rendered } # Detect if a directory contains Markdown source export def has_markdown_source [src_dir: string, markdown_config?: record] { let config = if ($markdown_config | is-empty) { {} } else { $markdown_config } let source_file = if "source_file" in ($config | columns) { $config.source_file } else { "content.md" } let markdown_path = ($src_dir | path join $source_file) ($markdown_path | path exists) } # Get Markdown source file path export def get_markdown_source_path [src_dir: string, markdown_config?: record] { let config = if ($markdown_config | is-empty) { {} } else { $markdown_config } let source_file = if "source_file" in ($config | columns) { $config.source_file } else { "content.md" } ($src_dir | path join $source_file) } # Validate Markdown processor availability export def check_markdown_processors [] { let processors = [ { name: "pulldown-cmark", command: "pulldown-cmark", language: "Rust" }, { name: "comrak", command: "comrak", language: "Rust" }, { name: "pandoc", command: "pandoc", language: "Haskell" }, { name: "marked", command: "marked", language: "Node.js" }, { name: "markdown-it", command: "markdown-it", language: "Node.js" } ] $processors | each { |proc| { name: $proc.name, available: ((which $proc.command | length) > 0), language: $proc.language, command: $proc.command } } }