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