Skip to main content

grimoire_css_lib/
lib.rs

1//! Core library module that orchestrates the core functionality of the Grimoire CSS engine.
2//!
3//! This module provides two main functions:
4//! - [`start`] - Pure function that executes core CSS processing logic
5//! - [`start_in_memory`] - Function for processing CSS in memory without file I/O
6//! - [`start_as_cli`] - CLI wrapper with logging and user feedback (spinners, colors), it is _not idiomatic_
7//!   for a typical Rust library because it introduces side effects and depends on
8//!   console/UI crates. Use it only if you specifically want the same CLI behavior
9//!   outside of the main binary (e.g. in a Node.js wrapper or CLI application).
10//!
11//! Choose [`start`] for library usage and [`start_as_cli`] for CLI applications.
12
13mod buffer;
14mod commands;
15mod core;
16mod infrastructure;
17
18#[cfg(feature = "analyzer")]
19pub mod analyzer;
20
21pub mod transmutator;
22
23#[cfg(feature = "lsp")]
24pub mod lsp;
25
26#[cfg(feature = "mcp")]
27pub mod mcp;
28
29use commands::{handle_in_memory, process_mode_and_handle, process_mode_and_handle_with_options};
30use console::style;
31use core::{compiled_css::CompiledCssInMemory, config::ConfigInMemory};
32use indicatif::{ProgressBar, ProgressStyle};
33use infrastructure::{GrimoireCssDiagnostic, LightningCssOptimizer};
34use miette::GraphicalReportHandler;
35use std::path::Path;
36use std::time::{Duration, Instant};
37
38pub use core::{GrimoireCssError, color, component, config, spell::Spell};
39
40static GRIMM_CALM: &str = " |(• ε •)|";
41static GRIMM_HAPPY: &str = " ヽ(• ε •)ノ";
42static GRIMM_CURSED: &str = " |(x ~ x)|";
43static GRIMM_CASTING: [&str; 8] = [
44    GRIMM_CALM,
45    " (|¬ヘ¬)|",
46    " \\(°o°)/",
47    " (∩¬ロ¬)⊃━▪ ~",
48    " (∩¬ロ¬)⊃━▪ ~·",
49    " (∩¬ロ¬)⊃━▪ ~·•",
50    " (∩¬ロ¬)⊃━▪ ~·•●",
51    GRIMM_HAPPY,
52];
53
54/// Starts the Grimoire CSS system based on the given mode,
55/// **without** performing any CLI-specific side effects.
56///
57/// This function determines the current working directory, initializes
58/// the `LightningCSSOptimizer`, and then processes the mode, invoking the
59/// appropriate command handlers.
60///
61/// # Arguments
62///
63/// * `mode` - A string representing the mode of operation (e.g., "build", "init").
64///
65/// # Returns
66///
67/// * `Ok(())` - If the mode is processed successfully.
68/// * `Err(GrimoireCSSError)` - If there is an error during initialization or command execution.
69///
70/// # Errors
71///
72/// This function returns a `GrimoireCSSError` if the current directory cannot be determined,
73/// the optimizer initialization fails, or the mode processing encounters an error.
74///
75/// # Examples
76///
77/// ```ignore
78/// use grimoire_css_lib::start;
79/// if let Err(e) = start("build".to_string()) {
80///     eprintln!("Error: {e}");
81/// }
82/// ```
83pub fn start(mode: &str) -> Result<(), GrimoireCssError> {
84    let current_dir = std::env::current_dir()?;
85    let css_optimizer = LightningCssOptimizer::new(&current_dir)?;
86
87    process_mode_and_handle(mode, &current_dir, &css_optimizer)
88}
89
90/// Builds the project rooted at `root`.
91pub fn build(root: &Path) -> Result<(), GrimoireCssError> {
92    build_with_options(root, false)
93}
94
95/// Builds the project, optionally updating a mismatched configuration version.
96pub fn build_with_options(root: &Path, force_version_update: bool) -> Result<(), GrimoireCssError> {
97    let css_optimizer = LightningCssOptimizer::new(root)?;
98    process_mode_and_handle_with_options(
99        "build",
100        root,
101        &css_optimizer,
102        commands::CliOptions {
103            force_version_update,
104        },
105    )
106}
107
108/// Initializes the project rooted at `root`.
109pub fn init(root: &Path) -> Result<(), GrimoireCssError> {
110    commands::init_project(root, "init").map(|_| ())
111}
112
113/// Shortens spells in the project rooted at `root`.
114pub fn shorten(root: &Path) -> Result<(), GrimoireCssError> {
115    commands::shorten_project(root)
116}
117
118pub fn start_in_memory(
119    config: &ConfigInMemory,
120) -> Result<Vec<CompiledCssInMemory>, GrimoireCssError> {
121    let css_optimizer = LightningCssOptimizer::new_from(
122        config.browserslist_content.as_deref().unwrap_or_default(),
123    )?;
124
125    handle_in_memory(config, &css_optimizer)
126}
127
128/// Analyzer/LSP helper: same as [`start_in_memory`], but prints CSS in a human-readable format.
129///
130/// This is feature-gated to avoid affecting default runtime/CLI output.
131#[cfg(feature = "analyzer")]
132pub fn start_in_memory_pretty(
133    config: &ConfigInMemory,
134) -> Result<Vec<CompiledCssInMemory>, GrimoireCssError> {
135    let css_optimizer = LightningCssOptimizer::new_from_with_printer_minify(
136        config.browserslist_content.as_deref().unwrap_or_default(),
137        false,
138    )?;
139
140    handle_in_memory(config, &css_optimizer)
141}
142
143/// Public function to read the saved messages from the buffer.
144/// This function is accessible from the main.rs (or any other crate/binary)
145/// for reading the buffer content.
146pub fn get_logged_messages() -> Vec<String> {
147    buffer::read_messages()
148}
149
150/// A convenience function that **simulates CLI behavior** (timing, logging, spinner)
151/// but is placed in the library crate to avoid duplicating code in multiple binaries
152/// or wrappers.
153///
154/// # Warning
155///
156/// - This is **not** an idiomatic approach for a typical Rust library since it
157///   introduces console-based side effects.
158/// - If you do not want logs, spinners, or colorized output, **do not** call this
159///   function. Instead, call [`start`] directly.
160/// - This function **depends** on `console` and `indicatif` crates for styling
161///   and progress-bar support, which might not be desired in all contexts.
162///
163/// # Arguments
164///
165/// * `args` - A vector of strings typically representing command-line arguments.
166///   The first argument is expected to be the binary name, and the second argument
167///   the mode (e.g. "build", "init").
168///
169/// # Returns
170///
171/// * `Ok(())` on success, or an `Err(GrimoireCSSError)` if invalid arguments or runtime
172///   issues occur.
173///
174/// # Examples
175///
176/// ```ignore
177/// use grimoire_css_lib::start_as_cli;
178///
179/// // Typically used in a real CLI setup:
180/// let args = vec!["grimoire_css".to_string(), "build".to_string()];
181/// if let Err(err) = start_as_cli(args) {
182///     eprintln!("Failed: {err}");
183/// }
184/// ```
185pub fn start_as_cli(args: Vec<String>) -> Result<(), GrimoireCssError> {
186    let bin_name = args
187        .first()
188        .and_then(|s| Path::new(s).file_name())
189        .and_then(|n| n.to_str())
190        .unwrap_or("grimoire_css");
191
192    let help_text = || {
193        let usage = ["grimoire_css", "grim"]
194            .into_iter()
195            .map(|n| format!("  {n} <mode> [mode args]"))
196            .collect::<Vec<_>>()
197            .join("\n");
198
199        format!(
200            "Usage:\n{usage}\n\nModes:\n  build\n  init\n  shorten\n  transmute\n  fi\n\nUtilities:\n  -h, --help       Print help\n  -V, --version    Print version\n"
201        )
202    };
203
204    if args.get(1).is_some_and(|a| a == "--version" || a == "-V") {
205        println!("{bin_name} {}", env!("CARGO_PKG_VERSION"));
206        return Ok(());
207    }
208
209    if args
210        .get(1)
211        .is_some_and(|a| a == "--help" || a == "-h" || a == "help")
212    {
213        println!("{}", help_text());
214        return Ok(());
215    }
216
217    if let Some(result) = commands::process_machine_readable_mode(&args) {
218        return result;
219    }
220
221    println!();
222
223    println!(
224        "{}  Ritual initiated",
225        style(" Grimoire CSS ").white().on_color256(55).bright(),
226    );
227
228    // Check if the user provided at least one argument (mode)
229    if args.len() < 2 {
230        let message = format!(
231            "{}  {} ",
232            style(" Cursed! ").white().on_red().bright(),
233            format_args!("No mode provided")
234        );
235
236        println!();
237        println!("{GRIMM_CURSED}");
238        println!();
239        println!("{message}");
240        println!();
241        println!("{}", help_text());
242
243        return Err(GrimoireCssError::InvalidInput(message));
244    }
245
246    println!();
247
248    let pb = ProgressBar::new_spinner();
249    pb.set_style(ProgressStyle::default_spinner().tick_strings(&GRIMM_CASTING));
250    pb.enable_steady_tick(Duration::from_millis(220));
251    pb.set_draw_target(indicatif::ProgressDrawTarget::stdout_with_hz(10));
252
253    let start_time = Instant::now();
254
255    let mode = args[1].as_str();
256
257    let cli_options = commands::CliOptions {
258        force_version_update: mode == "build" && args.iter().any(|a| a == "--force-version-update"),
259    };
260
261    // Proceed with the main function, passing the first argument (mode)
262    let current_dir = std::env::current_dir()?;
263    let css_optimizer = LightningCssOptimizer::new(&current_dir)?;
264
265    match process_mode_and_handle_with_options(mode, &current_dir, &css_optimizer, cli_options) {
266        Ok(_) => {
267            pb.finish_and_clear();
268
269            print!("\r\x1b[2K{GRIMM_HAPPY}  Spells cast successfully.\n");
270
271            let duration = start_time.elapsed();
272
273            output_saved_messages();
274
275            println!();
276
277            println!(
278                "{}",
279                style(format!(
280                    "{}",
281                    style(format!(" Enchanted in {duration:.2?}! "))
282                        .white()
283                        .on_color256(55)
284                        .bright(),
285                ))
286            );
287
288            println!();
289
290            Ok(())
291        }
292        Err(e) => {
293            pb.finish_and_clear();
294            print!("\r\x1b[2K{GRIMM_CURSED}\n");
295
296            println!();
297            println!("{}", style(" Cursed! ").white().on_red().bright());
298            println!();
299
300            let diagnostic: GrimoireCssDiagnostic = (&e).into();
301            let mut out = String::new();
302            GraphicalReportHandler::new()
303                .render_report(&mut out, &diagnostic)
304                .unwrap();
305            println!("{out}");
306
307            Err(e)
308        }
309    }
310}
311
312fn output_saved_messages() {
313    let messages = get_logged_messages();
314
315    if !messages.is_empty() {
316        println!();
317        for msg in &messages {
318            println!("  • {msg}");
319        }
320    }
321}