- Add 6 syntax highlighting themes (dracula, gruvbox, nord, github) - Rewrite css.rs to use lightningcss bundler for @import resolution - Theme CSS is inlined at build time, producing single bundled output
129 lines
3.4 KiB
Rust
129 lines
3.4 KiB
Rust
//! CSS processing via lightningcss.
|
|
//!
|
|
//! Provides CSS bundling and minification. The bundler resolves `@import`
|
|
//! rules at build time, inlining imported files into a single output.
|
|
|
|
use lightningcss::bundler::{Bundler, FileProvider};
|
|
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions};
|
|
use std::path::Path;
|
|
|
|
/// Bundle and minify a CSS file, resolving all `@import` rules.
|
|
///
|
|
/// This function:
|
|
/// 1. Reads the CSS file at `path`
|
|
/// 2. Resolves and inlines all `@import` rules (relative to source file)
|
|
/// 3. Minifies the combined output
|
|
///
|
|
/// Returns minified CSS string on success, or an error message on failure.
|
|
pub fn bundle_css(path: &Path) -> Result<String, String> {
|
|
let fs = FileProvider::new();
|
|
let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
|
|
|
|
let mut stylesheet = bundler
|
|
.bundle(path)
|
|
.map_err(|e| format!("bundle error: {e}"))?;
|
|
|
|
stylesheet
|
|
.minify(MinifyOptions::default())
|
|
.map_err(|e| format!("minify error: {e}"))?;
|
|
|
|
let result = stylesheet
|
|
.to_css(PrinterOptions {
|
|
minify: true,
|
|
..Default::default()
|
|
})
|
|
.map_err(|e| format!("print error: {e}"))?;
|
|
|
|
Ok(result.code)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_bundle_minifies() {
|
|
let dir = TempDir::new().unwrap();
|
|
let css_path = dir.path().join("test.css");
|
|
fs::write(
|
|
&css_path,
|
|
r#"
|
|
.foo {
|
|
color: red;
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let output = bundle_css(&css_path).unwrap();
|
|
|
|
// Should be minified (whitespace removed)
|
|
assert!(output.contains(".foo"));
|
|
assert!(output.contains("red"));
|
|
assert!(!output.contains('\n'));
|
|
}
|
|
|
|
#[test]
|
|
fn test_bundle_resolves_imports() {
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
// Create imported file
|
|
let imported_path = dir.path().join("colors.css");
|
|
fs::write(
|
|
&imported_path,
|
|
r#"
|
|
:root {
|
|
--primary: blue;
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
// Create main file that imports colors.css
|
|
let main_path = dir.path().join("main.css");
|
|
fs::write(
|
|
&main_path,
|
|
r#"
|
|
@import "colors.css";
|
|
|
|
.btn {
|
|
color: var(--primary);
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let output = bundle_css(&main_path).unwrap();
|
|
|
|
// Should contain content from both files
|
|
assert!(output.contains("--primary"));
|
|
assert!(output.contains("blue"));
|
|
assert!(output.contains(".btn"));
|
|
// Should NOT contain @import directive
|
|
assert!(!output.contains("@import"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_bundle_removes_comments() {
|
|
let dir = TempDir::new().unwrap();
|
|
let css_path = dir.path().join("test.css");
|
|
fs::write(
|
|
&css_path,
|
|
r#"
|
|
/* This is a comment */
|
|
.bar { background: blue; }
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let output = bundle_css(&css_path).unwrap();
|
|
|
|
// Comment should be removed
|
|
assert!(!output.contains("This is a comment"));
|
|
// Rule should remain
|
|
assert!(output.contains(".bar"));
|
|
}
|
|
}
|