eure-cli 0.1.4

Command-line tool for Eure format conversion and validation
use catppuccin::{FlavorColors, PALETTE};
use eure::query::{
    GetSemanticTokens, SemanticToken, SemanticTokenModifier, SemanticTokenType, TextFile,
    TextFileContent, build_runtime,
};
use eure::query_flow::DurabilityLevel;
use maud::{Markup, html};

use crate::util::{display_path, handle_query_error, read_input};

#[derive(clap::Args)]
pub struct Args {
    /// Path to Eure file (use '-' or omit for stdin)
    pub file: Option<String>,

    /// Catppuccin theme variant
    #[arg(short, long, value_enum, default_value = "frappe")]
    pub theme: Theme,
}

#[derive(clap::ValueEnum, Clone, Copy, Debug)]
pub enum Theme {
    Mocha,
    Macchiato,
    Frappe,
    Latte,
}

pub fn run(args: Args) {
    // 1. Read input
    let contents = match read_input(args.file.as_deref()) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{e}");
            std::process::exit(1);
        }
    };

    // Create query runtime
    let runtime = build_runtime();

    let file = TextFile::from_path(display_path(args.file.as_deref()).into());
    runtime.resolve_asset(
        file.clone(),
        TextFileContent(contents.clone()),
        DurabilityLevel::Static,
    );

    // Get semantic tokens
    let tokens = match runtime.query(GetSemanticTokens::new(file)) {
        Ok(result) => result,
        Err(e) => handle_query_error(&runtime, e),
    };

    // 4. Get color palette
    let palette = match args.theme {
        Theme::Mocha => PALETTE.mocha.colors,
        Theme::Macchiato => PALETTE.macchiato.colors,
        Theme::Frappe => PALETTE.frappe.colors,
        Theme::Latte => PALETTE.latte.colors,
    };

    // 5. Generate HTML using Maud
    let markup = generate_html(&contents, &tokens, palette, args.theme);
    println!("{}", markup.into_string());
}

fn generate_html(
    contents: &str,
    tokens: &[SemanticToken],
    palette: FlavorColors,
    theme: Theme,
) -> Markup {
    let bg = palette.base.hex;
    let fg = palette.text.hex;
    let theme_name = match theme {
        Theme::Mocha => "Mocha",
        Theme::Macchiato => "Macchiato",
        Theme::Frappe => "Frappé",
        Theme::Latte => "Latte",
    };

    html! {
        (maud::PreEscaped(format!("<!-- Generated by eure CLI (https://eure.dev) with Catppuccin {} theme -->\n", theme_name)))
        pre style=(format!("background: {}; color: {}; padding: 1em; border-radius: 0.5em; overflow-x: auto;", bg, fg)) {
            code {
                @for (i, token) in tokens.iter().enumerate() {
                    // Print whitespace before token (ESCAPED to prevent XSS)
                    @if i == 0 && token.start > 0 {
                        (&contents[0..token.start as usize])
                    } @else if i > 0 {
                        @let prev_end = (tokens[i-1].start + tokens[i-1].length) as usize;
                        @let curr_start = token.start as usize;
                        @if prev_end < curr_start {
                            (&contents[prev_end..curr_start])
                        }
                    }

                    // Print colored token (ESCAPED to prevent XSS)
                    @let start = token.start as usize;
                    @let end = start + token.length as usize;
                    @let text = &contents[start..end];
                    @let color = token_type_to_color(token.token_type, palette);
                    span style=(token_style(token, color)) { (text) }
                }

                // Print remaining content after last token (ESCAPED to prevent XSS)
                @if let Some(last) = tokens.last() {
                    @let last_end = (last.start + last.length) as usize;
                    @if last_end < contents.len() {
                        (&contents[last_end..])
                    }
                }
            }
        }
    }
}

fn token_style(token: &SemanticToken, color: catppuccin::Hex) -> String {
    let is_section_header = token.modifiers & SemanticTokenModifier::SectionHeader.bitmask() != 0;
    if is_section_header {
        format!("color: {}; font-weight: bold", color)
    } else {
        format!("color: {}", color)
    }
}

fn token_type_to_color(token_type: SemanticTokenType, palette: FlavorColors) -> catppuccin::Hex {
    match token_type {
        SemanticTokenType::Keyword => palette.mauve.hex, // Purple for keywords
        SemanticTokenType::Number => palette.peach.hex,  // Peach for numbers
        SemanticTokenType::String => palette.green.hex,  // Green for strings
        SemanticTokenType::Comment => palette.overlay0.hex, // Muted for comments
        SemanticTokenType::Operator => palette.lavender.hex, // Subtle purple for operators
        SemanticTokenType::Property => palette.text.hex, // Base text color for properties
        SemanticTokenType::Punctuation => palette.overlay2.hex, // Light gray for punctuation
        SemanticTokenType::Macro => palette.teal.hex,    // Teal for code blocks
        SemanticTokenType::Decorator => palette.pink.hex, // Pink for decorators
        SemanticTokenType::SectionMarker => palette.red.hex, // Red for @
        SemanticTokenType::ExtensionMarker => palette.pink.hex, // Strong pink for $
        SemanticTokenType::ExtensionIdent => palette.rosewater.hex, // Light pink for extension ident
    }
}