Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ pin-project = { version = "1.1.10" }
postcard = { version = "1.1.3", default-features = false }
serde_urlencoded = "0.7"
form_urlencoded = "1.2.1"
winnow = "0.5.31"

# desktop
wry = { version = "0.53.5", default-features = false }
Expand Down Expand Up @@ -589,6 +590,11 @@ name = "svg"
path = "examples/02-building-ui/svg.rs"
doc-scrape-examples = true

[[example]]
name = "css_modules"
path = "examples/03-assets-styling/css_modules.rs"
doc-scrape-examples = true

[[example]]
name = "custom_assets"
path = "examples/03-assets-styling/custom_assets.rs"
Expand Down
33 changes: 33 additions & 0 deletions examples/03-assets-styling/css_modules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! This example shows how to use css modules with the `styles!` macro. Css modules convert css
//! class names to unique names to avoid class name collisions.

use dioxus::prelude::*;

fn main() {
dioxus::launch(app);
}

fn app() -> Element {
// Each `style!` macro will generate a `Styles` struct in the current scope
styles!("/examples/assets/css_module1.css");

mod other {
use dioxus::prelude::*;
// Multiple `styles!` macros can be used in the same scope by placing them in modules
styles!(
"/examples/assets/css_module2.css",
// `styles!` can take `AssetOptions` as well
AssetOptions::css_module()
.with_minify(true)
.with_preload(false)
);
}

rsx! {
div { class: Styles::container,
div { class: other::Styles::test, "Hello, world!" }
div { class: other::Styles::highlight, "This is highlighted" }
div { class: Styles::global_class, "This uses a global class (no hash)" }
}
}
}
11 changes: 11 additions & 0 deletions examples/assets/css_module1.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.container {
background-color: lightblue;
padding: 20px;
border-radius: 8px;
}

/* The `:global` selector can be used to define global styles that are not scoped */
:global(.global-class) {
color: red;
font-weight: bold;
}
9 changes: 9 additions & 0 deletions examples/assets/css_module2.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.test {
font-size: 24px;
color: darkblue;
}

.highlight {
background-color: yellow;
padding: 5px;
}
48 changes: 22 additions & 26 deletions packages/cli-opt/src/css.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use lightningcss::{
stylesheet::{MinifyOptions, ParserOptions, StyleSheet},
targets::{Browsers, Targets},
};
use manganis_core::{CssAssetOptions, CssModuleAssetOptions};
use manganis_core::{create_module_hash, transform_css, CssAssetOptions, CssModuleAssetOptions};

pub(crate) fn process_css(
css_options: &CssAssetOptions,
Expand Down Expand Up @@ -46,43 +46,39 @@ pub(crate) fn process_css(
pub(crate) fn process_css_module(
css_options: &CssModuleAssetOptions,
source: &Path,
final_path: &Path,
output_path: &Path,
) -> anyhow::Result<()> {
let mut css = std::fs::read_to_string(source)?;
let css = std::fs::read_to_string(source)?;

// Collect the file hash name.
let mut src_name = source
.file_name()
.and_then(|x| x.to_str())
.ok_or(anyhow!("Failed to read name of css module source file."))?
.ok_or_else(|| {
anyhow!(
"Failed to read name of css module file `{}`.",
source.display()
)
})?
.strip_suffix(".css")
.unwrap()
.ok_or_else(|| {
anyhow!(
"Css module file `{}` should end with a `.css` suffix.",
source.display(),
)
})?
.to_string();

src_name.push('-');

let out_name = final_path
.file_name()
.and_then(|x| x.to_str())
.ok_or(anyhow!("Failed to read name of css module output file."))?
.strip_suffix(".css")
.unwrap();

let hash = out_name
.strip_prefix(&src_name)
.ok_or(anyhow!("Failed to read hash of css module."))?;

// Rewrite CSS idents with ident+hash.
let (classes, ids) = manganis_core::collect_css_idents(&css);

for class in classes {
css = css.replace(&format!(".{class}"), &format!(".{class}{hash}"));
}

for id in ids {
css = css.replace(&format!("#{id}"), &format!("#{id}{hash}"));
}
let hash = create_module_hash(source);
let css = transform_css(css.as_str(), hash.as_str()).map_err(|error| {
anyhow!(
"Invalid css for file `{}`\nError:\n{}",
source.display(),
error
)
})?;

// Minify CSS
let css = if css_options.minified() {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-opt/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(crate) fn process_file_to_with_options(
process_css(options, source, &temp_path)?;
}
ResolvedAssetType::CssModule(options) => {
process_css_module(options, source, output_path, &temp_path)?;
process_css_module(options, source, &temp_path)?;
}
ResolvedAssetType::Scss(options) => {
process_scss(options, source, &temp_path)?;
Expand Down
1 change: 1 addition & 0 deletions packages/manganis/manganis-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ serde = { workspace = true, features = ["derive"] }
const-serialize = { workspace = true, features = ["serde"] }
dioxus-core-types = { workspace = true, optional = true }
dioxus-cli-config = { workspace = true, optional = true }
winnow = { workspace = true }

[dev-dependencies]
manganis = { workspace = true }
Expand Down
108 changes: 11 additions & 97 deletions packages/manganis/manganis-core/src/css_module.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::Path,
};

use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst;
use std::collections::HashSet;

/// Options for a css module asset
#[derive(
Expand Down Expand Up @@ -91,100 +95,10 @@ impl AssetOptionsBuilder<CssModuleAssetOptions> {
}
}

/// Collect CSS classes & ids.
///
/// This is a rudementary css classes & ids collector.
/// Idents used only in media queries will not be collected. (not support yet)
///
/// There are likely a number of edge cases that will show up.
///
/// Returns `(HashSet<Classes>, HashSet<Ids>)`
pub fn collect_css_idents(css: &str) -> (HashSet<String>, HashSet<String>) {
const ALLOWED: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";

let mut classes = HashSet::new();
let mut ids = HashSet::new();

// Collected ident name and true for ids.
let mut start: Option<(String, bool)> = None;

// True if we have the first comment start delimiter `/`
let mut comment_start = false;
// True if we have the first comment end delimiter '*'
let mut comment_end = false;
// True if we're in a comment scope.
let mut in_comment_scope = false;

// True if we're in a block scope: `#hi { this is block scope }`
let mut in_block_scope = false;

// If we are currently collecting an ident:
// - Check if the char is allowed, put it into the ident string.
// - If not allowed, finalize the ident string and reset start.
// Otherwise:
// Check if character is a `.` or `#` representing a class or string, and start collecting.
for (_byte_index, c) in css.char_indices() {
if let Some(ident) = start.as_mut() {
if ALLOWED.find(c).is_some() {
// CSS ignore idents that start with a number.
// 1. Difficult to process
// 2. Avoid false positives (transition: 0.5s)
if ident.0.is_empty() && c.is_numeric() {
start = None;
continue;
}

ident.0.push(c);
} else {
match ident.1 {
true => ids.insert(ident.0.clone()),
false => classes.insert(ident.0.clone()),
};

start = None;
}
} else {
// Handle entering an exiting scopede.
match c {
// Mark as comment scope if we have comment start: /*
'*' if comment_start => {
comment_start = false;
in_comment_scope = true;
}
// Mark start of comment end if in comment scope: */
'*' if in_comment_scope => comment_end = true,
// Mark as comment start if not in comment scope and no comment start, mark comment_start
'/' if !in_comment_scope => {
comment_start = true;
}
// If we get the closing delimiter, mark as non-comment scope.
'/' if comment_end => {
in_comment_scope = false;
comment_start = false;
comment_end = false;
}
// Entering & Exiting block scope.
'{' => in_block_scope = true,
'}' => in_block_scope = false,
// Any other character, reset comment start and end if not in scope.
_ => {
comment_start = false;
comment_end = false;
}
}

// No need to process this char if in bad scope.
if in_comment_scope || in_block_scope {
continue;
}

match c {
'.' => start = Some((String::new(), false)),
'#' => start = Some((String::new(), true)),
_ => {}
}
}
}

(classes, ids)
pub fn create_module_hash(css_path: &Path) -> String {
let path_string = css_path.to_string_lossy();
let mut hasher = DefaultHasher::new();
path_string.hash(&mut hasher);
let hash = hasher.finish();
format!("{:016x}", hash)[..8].to_string()
}
Loading
Loading