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 {
pub file: Option<String>,
#[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) {
let contents = match read_input(args.file.as_deref()) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
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,
);
let tokens = match runtime.query(GetSemanticTokens::new(file)) {
Ok(result) => result,
Err(e) => handle_query_error(&runtime, e),
};
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,
};
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() {
@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])
}
}
@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) }
}
@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, SemanticTokenType::Number => palette.peach.hex, SemanticTokenType::String => palette.green.hex, SemanticTokenType::Comment => palette.overlay0.hex, SemanticTokenType::Operator => palette.lavender.hex, SemanticTokenType::Property => palette.text.hex, SemanticTokenType::Punctuation => palette.overlay2.hex, SemanticTokenType::Macro => palette.teal.hex, SemanticTokenType::Decorator => palette.pink.hex, SemanticTokenType::SectionMarker => palette.red.hex, SemanticTokenType::ExtensionMarker => palette.pink.hex, SemanticTokenType::ExtensionIdent => palette.rosewater.hex, }
}