Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(bundling): Various Rolldown related assets fixes (#23)

* fix(bundling): Set proper module types for various assets

* feat: rework Tailwind as a Rolldown plugin

* chore: some todos

* fix: hack for ts

* fix: various stuff

authored by

Erika and committed by
GitHub
025c2a3f 1e8fcbdb

+228 -88
+1
Cargo.lock
··· 2439 2439 "lol_html", 2440 2440 "maud", 2441 2441 "maudit-macros", 2442 + "oxc_sourcemap", 2442 2443 "pulldown-cmark", 2443 2444 "rustc-hash", 2444 2445 "serde",
+1
crates/maudit/Cargo.toml
··· 40 40 dyn-eq = "0.1.3" 41 41 thiserror = "2.0.9" 42 42 blake3 = "1.8.2" 43 + oxc_sourcemap = "4.1.0"
+82 -58
crates/maudit/src/assets.rs
··· 1 1 use dyn_eq::DynEq; 2 - use log::info; 3 2 use rustc_hash::FxHashSet; 4 3 use std::hash::Hash; 5 4 use std::path::Path; 6 - use std::process::Command; 7 - use std::time::SystemTime; 8 5 use std::{fs, path::PathBuf}; 9 6 10 7 #[derive(Default)] ··· 17 14 pub(crate) included_scripts: Vec<Script>, 18 15 19 16 pub(crate) assets_dir: PathBuf, 20 - pub(crate) tailwind_path: PathBuf, 21 17 } 22 18 23 19 impl PageAssets { 24 - /// Add an image to the page assets, causing the file to be created in the output directory. 20 + /// Add an image to the page assets, causing the file to be created in the output directory. The image is resolved relative to the current working directory. 25 21 /// 26 22 /// The image will not automatically be included in the page, but can be included through the `.url()` method on the returned `Image` object. 27 23 /// ··· 53 49 *image 54 50 } 55 51 56 - /// Add a script to the page assets, causing the file to be created in the output directory. 52 + /// Add a script to the page assets, causing the file to be created in the output directory. The script is resolved relative to the current working directory. 57 53 /// 58 54 /// The script will not automatically be included in the page, but can be included through the `.url()` method on the returned `Script` object. 59 55 /// Alternatively, a script can be included automatically using the [PageAssets::include_script] method instead. ··· 75 71 script 76 72 } 77 73 78 - /// Include a script in the page 74 + /// Include a script in the page. The script is resolved relative to the current working directory. 79 75 /// 80 76 /// This method will automatically include the script in the `<head>` of the page, if it exists. If the page does not include a `<head>` tag, at this time this method will silently fail. 81 77 /// ··· 95 91 self.included_scripts.push(script); 96 92 } 97 93 98 - /// Add a style to the page assets, causing the file to be created in the output directory. 94 + /// Add a style to the page assets, causing the file to be created in the output directory. The style is resolved relative to the current working directory. 95 + /// 96 + /// The style will not automatically be included in the page, but can be included through the `.url()` method on the returned `Style` object. 97 + /// Alternatively, a style can be included automatically using the [PageAssets::include_style] method instead. 98 + /// 99 + /// Subsequent calls to this method using the same path will return the same style, as such, the value returned by this method can be cloned and used multiple times without issue. this method is equivalent to calling `add_style_with_options` with the default `StyleOptions` and is purely provided for convenience. 100 + pub fn add_style<P>(&mut self, style_path: P) -> Style 101 + where 102 + P: Into<PathBuf>, 103 + { 104 + self.add_style_with_options(style_path, StyleOptions::default()) 105 + } 106 + 107 + /// Add a style to the page assets, causing the file to be created in the output directory. The style is resolved relative to the current working directory. 99 108 /// 100 109 /// The style will not automatically be included in the page, but can be included through the `.url()` method on the returned `Style` object. 101 110 /// 102 - /// Subsequent calls to this function using the same path will return the same style, as such, the value returned by this function can be cloned and used multiple times without issue. 103 - pub fn add_style<P>(&mut self, style_path: P, tailwind: bool) -> Style 111 + /// Subsequent calls to this method using the same path will return the same style, as such, the value returned by this method can be cloned and used multiple times without issue. 112 + pub fn add_style_with_options<P>(&mut self, style_path: P, options: StyleOptions) -> Style 104 113 where 105 114 P: Into<PathBuf>, 106 115 { 107 116 let path = style_path.into(); 108 117 let style = Style { 109 118 path: path.clone(), 110 - tailwind, 111 119 assets_dir: self.assets_dir.clone(), 112 - tailwind_path: self.tailwind_path.clone(), 113 120 hash: calculate_hash(&path), 121 + tailwind: options.tailwind, 114 122 }; 115 123 116 124 self.styles.insert(style.clone()); ··· 122 130 /// 123 131 /// This method will automatically include the style in the `<head>` of the page, if it exists. If the page does not include a `<head>` tag, at this time this method will silently fail. 124 132 /// 125 - /// Subsequent calls to this function using the same path will result in the same style being included multiple times. 126 - pub fn include_style<P>(&mut self, style_path: P, tailwind: bool) 133 + /// Subsequent calls to this method using the same path will result in the same style being included multiple times. This method is equivalent to calling `include_style_with_options` with the default `StyleOptions` and is purely provided for convenience. 134 + pub fn include_style<P>(&mut self, style_path: P) 135 + where 136 + P: Into<PathBuf>, 137 + { 138 + self.include_style_with_options(style_path, StyleOptions::default()) 139 + } 140 + 141 + /// Include a style in the page 142 + /// 143 + /// This method will automatically include the style in the `<head>` of the page, if it exists. If the page does not include a `<head>` tag, at this time this method will silently fail. 144 + /// 145 + /// Subsequent calls to this method using the same path will result in the same style being included multiple times. 146 + pub fn include_style_with_options<P>(&mut self, style_path: P, options: StyleOptions) 127 147 where 128 148 P: Into<PathBuf>, 129 149 { 130 150 let path = style_path.into(); 131 151 let style = Style { 132 152 path: path.clone(), 133 - tailwind, 134 153 assets_dir: self.assets_dir.clone(), 135 - tailwind_path: self.tailwind_path.clone(), 136 154 hash: calculate_hash(&path), 155 + tailwind: options.tailwind, 137 156 }; 138 157 139 158 self.styles.insert(style.clone()); ··· 158 177 String::new() 159 178 } 160 179 161 - fn final_file_name(&self) -> String { 162 - let file_stem = self.path().file_stem().unwrap().to_str().unwrap(); 163 - let extension = self 164 - .path() 180 + // TODO: I don't like these next two methods for scripts and styles, we should get this from Rolldown somehow, but I don't know how. 181 + // Our architecture is such that bundling runs after pages, so we can't know the final extension until then. We can't, and I don't want 182 + // to make it so we get assets beforehand because it'd make it less convenient and essentially cause us to act like a bundling framework. 183 + // 184 + // Perhaps it should be done as a post-processing step, like includes, but that'd require moving route finalization to after bundling, 185 + // which I'm not sure I want to do either. Plus, it'd be pretty slow if you have a layout on every page that includes a style/script (a fairly common case). 186 + // 187 + // An additional benefit would with that would also be to be able to avoid generating hashes for these files, but that's a smaller win. 188 + // 189 + // I don't know! - erika, 2025-09-01 190 + 191 + fn final_extension(&self) -> String { 192 + self.path() 165 193 .extension() 166 194 .map(|ext| ext.to_str().unwrap()) 167 - .unwrap_or(""); 195 + .unwrap_or_default() 196 + .to_owned() 197 + } 198 + 199 + fn final_file_name(&self) -> String { 200 + let file_stem = self.path().file_stem().unwrap().to_str().unwrap(); 201 + let extension = self.final_extension(); 168 202 169 203 if extension.is_empty() { 170 204 format!("{}.{}", file_stem, self.hash()) ··· 177 211 fn calculate_hash(path: &PathBuf) -> String { 178 212 let content = fs::read(path).unwrap(); 179 213 214 + // TODO: Consider using xxhash for both performance and to match Rolldown's hashing 180 215 let mut hasher = blake3::Hasher::new(); 181 216 hasher.update(&content); 182 217 hasher.update(path.to_string_lossy().as_bytes()); ··· 269 304 fn hash(&self) -> String { 270 305 self.hash.clone() 271 306 } 307 + 308 + fn final_extension(&self) -> String { 309 + let current_extension = self 310 + .path() 311 + .extension() 312 + .and_then(|ext| ext.to_str()) 313 + .unwrap_or_default(); 314 + 315 + match current_extension { 316 + "ts" => "js", 317 + ext => ext, 318 + } 319 + .to_string() 320 + } 321 + } 322 + 323 + #[derive(Clone, PartialEq, Eq, Hash, Default)] 324 + pub struct StyleOptions { 325 + pub tailwind: bool, 272 326 } 273 327 274 328 #[derive(Clone, PartialEq, Eq, Hash)] 275 329 #[non_exhaustive] 276 330 pub struct Style { 277 331 pub path: PathBuf, 278 - pub(crate) tailwind: bool, 279 332 pub(crate) assets_dir: PathBuf, 280 - pub(crate) tailwind_path: PathBuf, 281 333 pub(crate) hash: String, 334 + pub(crate) tailwind: bool, 282 335 } 283 336 284 337 impl InternalAsset for Style { ··· 305 358 self.hash.clone() 306 359 } 307 360 308 - fn process(&self, _: &Path, tmp_dir: &Path) -> Option<String> { 309 - // TODO: Detect tailwind automatically 310 - if self.tailwind { 311 - let tmp_path = tmp_dir.join(self.path.file_name().unwrap()); 312 - let tmp_path_str = tmp_path.to_str().unwrap().to_string(); 313 - 314 - let start_tailwind = SystemTime::now(); 315 - let tailwind_output = Command::new(self.tailwind_path.clone()) 316 - .args(["--input", self.path.to_str().unwrap()]) 317 - .args(["--output", &tmp_path_str]) 318 - .arg("--minify") // TODO: Allow disabling minification 319 - .output() 320 - .expect("failed to execute process"); 321 - 322 - info!("Tailwind took {:?}", start_tailwind.elapsed().unwrap()); 323 - 324 - if tailwind_output.status.success() { 325 - return Some(tmp_path_str); 326 - } 327 - } 328 - 361 + fn process(&self, _: &Path, _: &Path) -> Option<String> { 329 362 None 330 363 } 331 364 } ··· 351 384 let temp_dir = setup_temp_dir(); 352 385 let mut page_assets = PageAssets { 353 386 assets_dir: PathBuf::from("assets"), 354 - tailwind_path: PathBuf::from("tailwind"), 355 387 ..Default::default() 356 388 }; 357 - 358 - page_assets.add_style(temp_dir.join("style.css"), false); 389 + page_assets.add_style(temp_dir.join("style.css")); 359 390 360 391 assert!(page_assets.styles.len() == 1); 361 392 } ··· 365 396 let temp_dir = setup_temp_dir(); 366 397 let mut page_assets = PageAssets { 367 398 assets_dir: PathBuf::from("assets"), 368 - tailwind_path: PathBuf::from("tailwind"), 369 399 ..Default::default() 370 400 }; 371 401 372 - page_assets.include_style(temp_dir.join("style.css"), false); 402 + page_assets.include_style(temp_dir.join("style.css")); 373 403 374 404 assert!(page_assets.styles.len() == 1); 375 405 assert!(page_assets.included_styles.len() == 1); ··· 380 410 let temp_dir = setup_temp_dir(); 381 411 let mut page_assets = PageAssets { 382 412 assets_dir: PathBuf::from("assets"), 383 - tailwind_path: PathBuf::from("tailwind"), 384 413 ..Default::default() 385 414 }; 386 415 ··· 393 422 let temp_dir = setup_temp_dir(); 394 423 let mut page_assets = PageAssets { 395 424 assets_dir: PathBuf::from("assets"), 396 - tailwind_path: PathBuf::from("tailwind"), 397 425 ..Default::default() 398 426 }; 399 427 ··· 408 436 let temp_dir = setup_temp_dir(); 409 437 let mut page_assets = PageAssets { 410 438 assets_dir: PathBuf::from("assets"), 411 - tailwind_path: PathBuf::from("tailwind"), 412 439 ..Default::default() 413 440 }; 414 441 ··· 421 448 let temp_dir = setup_temp_dir(); 422 449 let mut page_assets = PageAssets { 423 450 assets_dir: PathBuf::from("assets"), 424 - tailwind_path: PathBuf::from("tailwind"), 425 451 ..Default::default() 426 452 }; 427 453 ··· 431 457 let script = page_assets.add_script(temp_dir.join("script.js")); 432 458 assert_eq!(script.url().unwrap().chars().next(), Some('/')); 433 459 434 - let style = page_assets.add_style(temp_dir.join("style.css"), false); 460 + let style = page_assets.add_style(temp_dir.join("style.css")); 435 461 assert_eq!(style.url().unwrap().chars().next(), Some('/')); 436 462 } 437 463 ··· 440 466 let temp_dir = setup_temp_dir(); 441 467 let mut page_assets = PageAssets { 442 468 assets_dir: PathBuf::from("assets"), 443 - tailwind_path: PathBuf::from("tailwind"), 444 469 ..Default::default() 445 470 }; 446 471 ··· 452 477 let script_hash = script.hash.clone(); 453 478 assert!(script.url().unwrap().contains(&script_hash)); 454 479 455 - let style = page_assets.add_style(temp_dir.join("style.css"), false); 480 + let style = page_assets.add_style(temp_dir.join("style.css")); 456 481 let style_hash = style.hash.clone(); 457 482 assert!(style.url().unwrap().contains(&style_hash)); 458 483 } ··· 462 487 let temp_dir = setup_temp_dir(); 463 488 let mut page_assets = PageAssets { 464 489 assets_dir: PathBuf::from("assets"), 465 - tailwind_path: PathBuf::from("tailwind"), 466 490 ..Default::default() 467 491 }; 468 492 ··· 474 498 let script_hash = script.hash.clone(); 475 499 assert!(script.build_path().to_string_lossy().contains(&script_hash)); 476 500 477 - let style = page_assets.add_style(temp_dir.join("style.css"), false); 501 + let style = page_assets.add_style(temp_dir.join("style.css")); 478 502 let style_hash = style.hash.clone(); 479 503 assert!(style.build_path().to_string_lossy().contains(&style_hash)); 480 504 }
+120 -18
crates/maudit/src/build.rs
··· 1 + use core::panic; 1 2 use std::{ 2 3 env, 3 4 fs::{self, remove_dir_all, File}, 4 5 io::{self, Write}, 5 6 path::{Path, PathBuf}, 7 + process::Command, 6 8 str::FromStr, 9 + sync::Arc, 7 10 time::{SystemTime, UNIX_EPOCH}, 8 11 }; 9 12 ··· 21 24 }; 22 25 use colored::{ColoredString, Colorize}; 23 26 use log::{info, trace}; 24 - use rolldown::{Bundler, BundlerOptions, InputItem}; 27 + use oxc_sourcemap::SourceMap; 28 + use rolldown::{ 29 + plugin::{HookUsage, Plugin}, 30 + Bundler, BundlerOptions, InputItem, ModuleType, 31 + }; 25 32 use rustc_hash::{FxHashMap, FxHashSet}; 26 33 27 34 use crate::assets::Asset; ··· 32 39 pub mod metadata; 33 40 pub mod options; 34 41 42 + #[derive(Debug)] 43 + struct TailwindPlugin { 44 + tailwind_path: String, 45 + tailwind_entries: Vec<PathBuf>, 46 + } 47 + 48 + impl Plugin for TailwindPlugin { 49 + fn name(&self) -> std::borrow::Cow<'static, str> { 50 + "builtin:tailwind".into() 51 + } 52 + 53 + fn register_hook_usage(&self) -> rolldown::plugin::HookUsage { 54 + HookUsage::Transform 55 + } 56 + 57 + async fn transform( 58 + &self, 59 + _ctx: rolldown::plugin::SharedTransformPluginContext, 60 + args: &rolldown::plugin::HookTransformArgs<'_>, 61 + ) -> rolldown::plugin::HookTransformReturn { 62 + if *args.module_type != ModuleType::Css { 63 + return Ok(None); 64 + } 65 + 66 + if self 67 + .tailwind_entries 68 + .iter() 69 + .any(|entry| entry.canonicalize().unwrap().to_string_lossy() == args.id) 70 + { 71 + let start_tailwind = SystemTime::now(); 72 + let tailwind_output = 73 + Command::new(&self.tailwind_path) 74 + .args(["--input", args.id]) 75 + .arg("--minify") // TODO: Allow disabling minification 76 + .arg("--map") // TODO: Allow disabling source maps 77 + .output() 78 + .unwrap_or_else(|e| { 79 + // TODO: Return a proper error instead of panicking 80 + panic!( 81 + "Failed to execute Tailwind CSS command, is it installed and is the path to its binary correct?\nCommand: '{}', Args: ['--input', '{}', '--minify', '--map']. Error: {}", 82 + &self.tailwind_path, 83 + args.id, 84 + e 85 + ) 86 + }); 87 + 88 + if !tailwind_output.status.success() { 89 + let stderr = String::from_utf8_lossy(&tailwind_output.stderr); 90 + let error_message = format!( 91 + "Tailwind CSS process failed with status {}: {}", 92 + tailwind_output.status, stderr 93 + ); 94 + panic!("{}", error_message); 95 + } 96 + 97 + info!("Tailwind took {:?}", start_tailwind.elapsed().unwrap()); 98 + 99 + let output = String::from_utf8_lossy(&tailwind_output.stdout); 100 + let (code, map) = if let Some((code, map)) = output.split_once("/*# sourceMappingURL") { 101 + (code.to_string(), Some(map.to_string())) 102 + } else { 103 + (output.to_string(), None) 104 + }; 105 + 106 + if let Some(map) = map { 107 + let source_map = SourceMap::from_json_string(&map).ok(); 108 + 109 + return Ok(Some(rolldown::plugin::HookTransformOutput { 110 + code: Some(code), 111 + map: source_map, 112 + ..Default::default() 113 + })); 114 + } 115 + 116 + return Ok(Some(rolldown::plugin::HookTransformOutput { 117 + code: Some(code), 118 + ..Default::default() 119 + })); 120 + } 121 + 122 + Ok(None) 123 + } 124 + } 125 + 35 126 pub fn execute_build( 36 127 routes: &[&dyn FullPage], 37 128 content_sources: &mut ContentSources, ··· 132 223 let route_start = SystemTime::now(); 133 224 let mut page_assets = assets::PageAssets { 134 225 assets_dir: options.assets_dir.clone().into(), 135 - tailwind_path: options.tailwind_binary_path.clone().into(), 136 226 ..Default::default() 137 227 }; 138 228 ··· 190 280 for params in routes { 191 281 let mut pages_assets = assets::PageAssets { 192 282 assets_dir: options.assets_dir.clone().into(), 193 - tailwind_path: options.tailwind_binary_path.clone().into(), 194 283 ..Default::default() 195 284 }; 196 285 let route_start = SystemTime::now(); ··· 286 375 name: Some( 287 376 script 288 377 .final_file_name() 289 - .strip_suffix(&format!( 290 - ".{}", 291 - script 292 - .path() 293 - .extension() 294 - .map(|ext| ext.to_str().unwrap()) 295 - .unwrap_or("") 296 - )) 378 + .strip_suffix(&format!(".{}", script.final_extension())) 297 379 .unwrap_or(&script.final_file_name()) 298 380 .to_string(), 299 381 ), ··· 302 384 .collect::<Vec<InputItem>>(); 303 385 304 386 if !bundler_inputs.is_empty() { 305 - let mut bundler = Bundler::new(BundlerOptions { 306 - input: Some(bundler_inputs), 307 - minify: Some(rolldown::RawMinifyOptions::Bool(true)), 308 - dir: Some(assets_dir.to_string_lossy().to_string()), 387 + let mut module_types_hashmap = FxHashMap::default(); 388 + module_types_hashmap.insert("woff".to_string(), ModuleType::Asset); 389 + module_types_hashmap.insert("woff2".to_string(), ModuleType::Asset); 309 390 310 - ..Default::default() 311 - }); 391 + let mut bundler = Bundler::with_plugins( 392 + BundlerOptions { 393 + input: Some(bundler_inputs), 394 + minify: Some(rolldown::RawMinifyOptions::Bool(true)), 395 + dir: Some(assets_dir.to_string_lossy().to_string()), 396 + module_types: Some(module_types_hashmap), 397 + 398 + ..Default::default() 399 + }, 400 + vec![Arc::new(TailwindPlugin { 401 + tailwind_path: options.tailwind_binary_path.clone(), 402 + tailwind_entries: build_pages_styles 403 + .iter() 404 + .filter_map(|style| { 405 + if style.tailwind { 406 + Some(style.path().clone()) 407 + } else { 408 + None 409 + } 410 + }) 411 + .collect::<Vec<PathBuf>>(), 412 + })], 413 + ); 312 414 313 415 let _result = bundler.write().await.unwrap(); 314 416 ··· 428 530 for script in included_scripts { 429 531 el.append( 430 532 &format!( 431 - "<script src=\"{}\"></script>", 533 + "<script src=\"{}\" type=\"module\"></script>", 432 534 script.url().unwrap_or_else(|| panic!( 433 535 "Failed to get URL for script: {:?}. This should not happen, please report this issue.", 434 536 script.path()
+4 -1
crates/maudit/src/build/options.rs
··· 39 39 pub output_dir: String, 40 40 pub assets_dir: String, 41 41 pub static_dir: String, 42 + /// Path to [the TailwindCSS CLI binary](https://tailwindcss.com/docs/installation/tailwind-cli). By default `tailwindcss`, which assumes you've installed it globally (for example, through Homebrew) and that it is in your `PATH`. 43 + /// 44 + /// This is commonly set to `./node_modules/.bin/tailwindcss` or similar, in order to use a locally installed version. 42 45 pub tailwind_binary_path: String, 43 46 /// Whether to clean the output directory before building. 44 47 /// ··· 68 71 output_dir: "dist".to_string(), 69 72 assets_dir: "_maudit".to_string(), 70 73 static_dir: "static".to_string(), 71 - tailwind_binary_path: "./node_modules/.bin/tailwindcss".to_string(), 74 + tailwind_binary_path: "tailwindcss".to_string(), 72 75 clean_output_dir: true, 73 76 } 74 77 }
+1 -1
crates/maudit/src/lib.rs
··· 6 6 //! </div> 7 7 8 8 // Modules the end-user will interact directly or indirectly with 9 - mod assets; 9 + pub mod assets; 10 10 pub mod content; 11 11 pub mod errors; 12 12 pub mod page;
+2 -1
crates/maudit/src/page.rs
··· 260 260 pub use super::{ 261 261 get_page_url, DynamicRouteContext, Page, RenderResult, RouteContext, RouteParams, 262 262 }; 263 + // TODO: Remove this internal re-export when possible 263 264 #[doc(hidden)] 264 265 pub use super::{FullPage, InternalPage}; 265 - pub use crate::assets::Asset; 266 + pub use crate::assets::{Asset, Image, Style, StyleOptions}; 266 267 pub use crate::content::MarkdownContent; 267 268 pub use maudit_macros::{route, Params}; 268 269 }
+3 -2
examples/kitchen-sink/src/pages/dynamic.rs
··· 1 - use maudit::page::{prelude::*, DynamicRouteContext}; 1 + use maudit::page::prelude::*; 2 2 3 3 use maud::html; 4 4 ··· 18 18 fn render(&self, ctx: &mut RouteContext) -> RenderResult { 19 19 let params = ctx.params::<Params>(); 20 20 let image = ctx.assets.add_image("data/social-card.png"); 21 - ctx.assets.include_style("data/tailwind.css", true); 21 + ctx.assets 22 + .include_style_with_options("data/tailwind.css", StyleOptions { tailwind: true }); 22 23 23 24 html! { 24 25 head {
+2 -1
examples/kitchen-sink/src/pages/endpoint.rs
··· 7 7 fn render(&self, ctx: &mut RouteContext) -> RenderResult { 8 8 let image = ctx.assets.add_image("data/logo.svg"); 9 9 let some_script = ctx.assets.add_script("data/script.js"); 10 - ctx.assets.include_style("data/tailwind.css", true); 10 + ctx.assets 11 + .include_style_with_options("data/tailwind.css", StyleOptions { tailwind: true }); 11 12 12 13 // Return some JSON 13 14 RenderResult::Text(format!(
+3 -1
examples/kitchen-sink/src/pages/index.rs
··· 11 11 fn render(&self, ctx: &mut RouteContext) -> RenderResult { 12 12 let image = ctx.assets.add_image("data/logo.svg"); 13 13 let script = ctx.assets.add_script("data/some_other_script.js"); 14 - let style = ctx.assets.add_style("data/tailwind.css", true); 14 + let style = ctx 15 + .assets 16 + .add_style_with_options("data/tailwind.css", StyleOptions { tailwind: true }); 15 17 16 18 let link_to_first_dynamic = 17 19 get_page_url(&DynamicExample, &DynamicExampleParams { page: 1 });
+6 -4
website/content/docs/styling.md
··· 19 19 20 20 impl Page<RouteParams, Markup> for Blog { 21 21 fn render(&self, ctx: &mut RouteContext) -> Markup { 22 - let style = ctx.assets.add_style("style.css", false); 22 + let style = ctx.assets.add_style("style.css"); 23 23 24 24 html! { 25 25 (style) // Generates <link rel="stylesheet" href="STYLE_URL" /> ··· 32 32 33 33 ```rs 34 34 fn render(&self, ctx: &mut RouteContext) -> Markup { 35 - ctx.assets.include_style("style.css", false); 35 + ctx.assets.include_style("style.css"); 36 36 37 37 layout( 38 38 html! { ··· 46 46 47 47 #### Tailwind support 48 48 49 - Maudit includes built-in support for [Tailwind CSS](https://tailwindcss.com/). To use it, pass `true` as the second argument to `add_style()` or `include_style()`. In the future, Maudit will automatically detect Tailwind CSS and enable it when needed. 49 + Maudit includes built-in support for [Tailwind CSS](https://tailwindcss.com/). To use it, use `add_style_with_options()` or `include_style_with_options()` with the `StyleOptions { tailwind: true }` option. 50 50 51 51 ```rs 52 52 fn render(&self, ctx: &mut RouteContext) -> Markup { 53 - ctx.assets.add_style("style.css", true); 53 + ctx.assets.add_style_with_options("style.css", StyleOptions { tailwind: true }); 54 54 55 55 html! { 56 56 div.bg-red-500 { ··· 59 59 } 60 60 } 61 61 ``` 62 + 63 + Maudit will automatically run Tailwind (using the binary provided at [`BuildOptions#tailwind_binary_path`](https://docs.rs/maudit/0.3.2/maudit/struct.BuildOptions.html#structfield.tailwind_binary_path)) on the specified CSS file. 62 64 63 65 Tailwind can then be configured normally, through native CSS in Tailwind 4.0, or through a `tailwind.config.js` file in earlier versions. 64 66
+3 -1
website/src/layout.rs
··· 5 5 use docs_sidebars::{left_sidebar, right_sidebar}; 6 6 7 7 pub use header::header; 8 + use maudit::assets::StyleOptions; 8 9 use maudit::content::MarkdownHeading; 9 10 use maudit::maud::generator; 10 11 use maudit::page::{RenderResult, RouteContext}; ··· 40 41 licenses: bool, 41 42 ctx: &mut RouteContext, 42 43 ) -> RenderResult { 43 - ctx.assets.include_style("assets/prin.css", true); 44 + ctx.assets 45 + .include_style_with_options("assets/prin.css", StyleOptions { tailwind: true }); 44 46 45 47 html! { 46 48 (DOCTYPE)