Scoped css (#5087)
* Create new css module parser * Use css module processing * Make hot reloading work for css_module macro * Change css_module macro shape and rename to styles * Add css_module example * Update styles macro documentation * Remove hidden from styles macro * Remove unneeded param * Remove unneeded visibility * Improve `styles!` macro documentation * Clippy * Remove unneeded method * Clippy * Clippy * Replace panic with anyhow * Update css_module macro to attribute macro * Update macro docs * chore: Cargo.lock * Clippy * respect semver by deprecating old func * update to latest winnow --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
34554766db
commit
9965220947
14 files changed
+908
-216
No files matched your search
Generated
+5
-4
@@ -11024,6 +11024,7 @@ dependencies = [
|
|||||||
"dioxus-core-types",
|
"dioxus-core-types",
|
||||||
"manganis",
|
"manganis",
|
||||||
"serde",
|
"serde",
|
||||||
|
"winnow 0.7.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -17868,7 +17869,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.12.1",
|
||||||
"toml_datetime 0.6.11",
|
"toml_datetime 0.6.11",
|
||||||
"winnow 0.5.40",
|
"winnow 0.5.31",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -17879,7 +17880,7 @@ checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.12.1",
|
||||||
"toml_datetime 0.6.11",
|
"toml_datetime 0.6.11",
|
||||||
"winnow 0.5.40",
|
"winnow 0.5.31",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -20526,9 +20527,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.5.40"
|
version = "0.5.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ pin-project = { version = "1.1.10" }
|
|||||||
postcard = { version = "1.1.3", default-features = false }
|
postcard = { version = "1.1.3", default-features = false }
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
form_urlencoded = "1.2.1"
|
form_urlencoded = "1.2.1"
|
||||||
|
winnow = "0.7.14"
|
||||||
|
|
||||||
# desktop
|
# desktop
|
||||||
wry = { version = "0.53.5", default-features = false }
|
wry = { version = "0.53.5", default-features = false }
|
||||||
@@ -597,6 +598,11 @@ name = "svg"
|
|||||||
path = "examples/02-building-ui/svg.rs"
|
path = "examples/02-building-ui/svg.rs"
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "css_modules"
|
||||||
|
path = "examples/03-assets-styling/css_modules.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "custom_assets"
|
name = "custom_assets"
|
||||||
path = "examples/03-assets-styling/custom_assets.rs"
|
path = "examples/03-assets-styling/custom_assets.rs"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! This example shows how to use css modules with the `css_module` 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 `css_module` macro will expand the annotated struct in the current scope
|
||||||
|
#[css_module("/examples/assets/css_module1.css")]
|
||||||
|
struct Styles;
|
||||||
|
|
||||||
|
#[css_module(
|
||||||
|
"/examples/assets/css_module2.css",
|
||||||
|
// `css_module` can take `AssetOptions` as well
|
||||||
|
AssetOptions::css_module()
|
||||||
|
.with_minify(true)
|
||||||
|
.with_preload(false)
|
||||||
|
)]
|
||||||
|
struct OtherStyles;
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { class: Styles::container,
|
||||||
|
div { class: OtherStyles::test, "Hello, world!" }
|
||||||
|
div { class: OtherStyles::highlight, "This is highlighted" }
|
||||||
|
div { class: Styles::global_class, "This uses a global class (no hash)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.test {
|
||||||
|
font-size: 24px;
|
||||||
|
color: darkblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background-color: yellow;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
+22
-26
@@ -8,7 +8,7 @@ use lightningcss::{
|
|||||||
stylesheet::{MinifyOptions, ParserOptions, StyleSheet},
|
stylesheet::{MinifyOptions, ParserOptions, StyleSheet},
|
||||||
targets::{Browsers, Targets},
|
targets::{Browsers, Targets},
|
||||||
};
|
};
|
||||||
use manganis_core::{CssAssetOptions, CssModuleAssetOptions};
|
use manganis_core::{create_module_hash, transform_css, CssAssetOptions, CssModuleAssetOptions};
|
||||||
|
|
||||||
pub(crate) fn process_css(
|
pub(crate) fn process_css(
|
||||||
css_options: &CssAssetOptions,
|
css_options: &CssAssetOptions,
|
||||||
@@ -46,43 +46,39 @@ pub(crate) fn process_css(
|
|||||||
pub(crate) fn process_css_module(
|
pub(crate) fn process_css_module(
|
||||||
css_options: &CssModuleAssetOptions,
|
css_options: &CssModuleAssetOptions,
|
||||||
source: &Path,
|
source: &Path,
|
||||||
final_path: &Path,
|
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut css = std::fs::read_to_string(source)?;
|
let css = std::fs::read_to_string(source)?;
|
||||||
|
|
||||||
// Collect the file hash name.
|
// Collect the file hash name.
|
||||||
let mut src_name = source
|
let mut src_name = source
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|x| x.to_str())
|
.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")
|
.strip_suffix(".css")
|
||||||
.unwrap()
|
.ok_or_else(|| {
|
||||||
|
anyhow!(
|
||||||
|
"Css module file `{}` should end with a `.css` suffix.",
|
||||||
|
source.display(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
src_name.push('-');
|
src_name.push('-');
|
||||||
|
|
||||||
let out_name = final_path
|
let hash = create_module_hash(source);
|
||||||
.file_name()
|
let css = transform_css(css.as_str(), hash.as_str()).map_err(|error| {
|
||||||
.and_then(|x| x.to_str())
|
anyhow!(
|
||||||
.ok_or(anyhow!("Failed to read name of css module output file."))?
|
"Invalid css for file `{}`\nError:\n{}",
|
||||||
.strip_suffix(".css")
|
source.display(),
|
||||||
.unwrap();
|
error
|
||||||
|
)
|
||||||
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}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minify CSS
|
// Minify CSS
|
||||||
let css = if css_options.minified() {
|
let css = if css_options.minified() {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub(crate) fn process_file_to_with_options(
|
|||||||
process_css(options, source, &temp_path)?;
|
process_css(options, source, &temp_path)?;
|
||||||
}
|
}
|
||||||
ResolvedAssetType::CssModule(options) => {
|
ResolvedAssetType::CssModule(options) => {
|
||||||
process_css_module(options, source, output_path, &temp_path)?;
|
process_css_module(options, source, &temp_path)?;
|
||||||
}
|
}
|
||||||
ResolvedAssetType::Scss(options) => {
|
ResolvedAssetType::Scss(options) => {
|
||||||
process_scss(options, source, &temp_path)?;
|
process_scss(options, source, &temp_path)?;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const-serialize-08 = { workspace = true, features = ["serde", "const-serialize-0
|
|||||||
const-serialize-07 = { workspace = true }
|
const-serialize-07 = { workspace = true }
|
||||||
dioxus-core-types = { workspace = true, optional = true }
|
dioxus-core-types = { workspace = true, optional = true }
|
||||||
dioxus-cli-config = { workspace = true, optional = true }
|
dioxus-cli-config = { workspace = true, optional = true }
|
||||||
|
winnow = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
manganis = { workspace = true }
|
manganis = { workspace = true }
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
|
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
|
||||||
use const_serialize_07 as const_serialize;
|
use const_serialize_07 as const_serialize;
|
||||||
use const_serialize_08::SerializeConst;
|
use const_serialize_08::SerializeConst;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
/// Options for a css module asset
|
/// Options for a css module asset
|
||||||
#[derive(
|
#[derive(
|
||||||
@@ -94,6 +99,15 @@ impl AssetOptionsBuilder<CssModuleAssetOptions> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a hash for a css module based on the file path
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect CSS classes & ids.
|
/// Collect CSS classes & ids.
|
||||||
///
|
///
|
||||||
/// This is a rudementary css classes & ids collector.
|
/// This is a rudementary css classes & ids collector.
|
||||||
@@ -102,6 +116,10 @@ impl AssetOptionsBuilder<CssModuleAssetOptions> {
|
|||||||
/// There are likely a number of edge cases that will show up.
|
/// There are likely a number of edge cases that will show up.
|
||||||
///
|
///
|
||||||
/// Returns `(HashSet<Classes>, HashSet<Ids>)`
|
/// Returns `(HashSet<Classes>, HashSet<Ids>)`
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.7.3",
|
||||||
|
note = "This function is no longer used by the css module system and will be removed in a future release."
|
||||||
|
)]
|
||||||
pub fn collect_css_idents(css: &str) -> (HashSet<String>, HashSet<String>) {
|
pub fn collect_css_idents(css: &str) -> (HashSet<String>, HashSet<String>) {
|
||||||
const ALLOWED: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
|
const ALLOWED: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,645 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use winnow::{
|
||||||
|
combinator::{alt, cut_err, delimited, opt, peek, preceded, repeat, terminated},
|
||||||
|
error::{ContextError, ErrMode, ParseError},
|
||||||
|
prelude::*,
|
||||||
|
stream::{AsChar, ContainsToken, Range},
|
||||||
|
token::{none_of, one_of, take_till, take_until, take_while},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// ```text
|
||||||
|
/// v----v inner span
|
||||||
|
/// :global(.class)
|
||||||
|
/// ^-------------^ outer span
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Global<'s> {
|
||||||
|
pub inner: &'s str,
|
||||||
|
pub outer: &'s str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum CssFragment<'s> {
|
||||||
|
Class(&'s str),
|
||||||
|
Global(Global<'s>),
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************************************************//
|
||||||
|
|
||||||
|
/// Parses and rewrites CSS class selectors with the hash applied.
|
||||||
|
/// Does not modify `:global(...)` selectors.
|
||||||
|
pub fn transform_css<'a>(
|
||||||
|
css: &'a str,
|
||||||
|
hash: &str,
|
||||||
|
) -> Result<String, ParseError<&'a str, ContextError>> {
|
||||||
|
let fragments = parse_css(css)?;
|
||||||
|
|
||||||
|
let mut new_css = String::with_capacity(css.len() * 2);
|
||||||
|
let mut cursor = css;
|
||||||
|
|
||||||
|
for fragment in fragments {
|
||||||
|
let (span, replace) = match fragment {
|
||||||
|
CssFragment::Class(class) => (class, Cow::Owned(apply_hash(class, hash))),
|
||||||
|
CssFragment::Global(Global { inner, outer }) => (outer, Cow::Borrowed(inner)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (before, after) = cursor.split_at(span.as_ptr() as usize - cursor.as_ptr() as usize);
|
||||||
|
cursor = &after[span.len()..];
|
||||||
|
new_css.push_str(before);
|
||||||
|
new_css.push_str(&replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_css.push_str(cursor);
|
||||||
|
Ok(new_css)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all the classes in the css files and their rewritten names.
|
||||||
|
/// Includes `:global(...)` classes where the name is not changed.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn get_class_mappings<'a>(
|
||||||
|
css: &'a str,
|
||||||
|
hash: &str,
|
||||||
|
) -> Result<Vec<(&'a str, Cow<'a, str>)>, ParseError<&'a str, ContextError>> {
|
||||||
|
let fragments = parse_css(css)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for c in fragments {
|
||||||
|
match c {
|
||||||
|
CssFragment::Class(class) => {
|
||||||
|
result.push((class, Cow::Owned(apply_hash(class, hash))));
|
||||||
|
}
|
||||||
|
CssFragment::Global(global) => {
|
||||||
|
let global_classes = resolve_global_inner_classes(global)?;
|
||||||
|
result.extend(
|
||||||
|
global_classes
|
||||||
|
.into_iter()
|
||||||
|
.map(|class| (class, Cow::Borrowed(class))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sort_by_key(|e| e.0);
|
||||||
|
result.dedup_by_key(|e| e.0);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_global_inner_classes<'a>(
|
||||||
|
global: Global<'a>,
|
||||||
|
) -> Result<Vec<&'a str>, ParseError<&'a str, ContextError>> {
|
||||||
|
let input = global.inner;
|
||||||
|
let fragments = selector.parse(input)?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for c in fragments {
|
||||||
|
match c {
|
||||||
|
CssFragment::Class(class) => result.push(class),
|
||||||
|
CssFragment::Global(_) => {
|
||||||
|
unreachable!("Top level parser should have already errored if globals are nested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_hash(class: &str, hash: &str) -> String {
|
||||||
|
format!("{}-{}", class, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************************************************//
|
||||||
|
|
||||||
|
pub fn parse_css(input: &str) -> Result<Vec<CssFragment<'_>>, ParseError<&str, ContextError>> {
|
||||||
|
style_rule_block_contents.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recognize_repeat<'s, O>(
|
||||||
|
range: impl Into<Range>,
|
||||||
|
f: impl Parser<&'s str, O, ErrMode<ContextError>>,
|
||||||
|
) -> impl Parser<&'s str, &'s str, ErrMode<ContextError>> {
|
||||||
|
repeat(range, f).fold(|| (), |_, _| ()).take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ws<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
recognize_repeat(
|
||||||
|
0..,
|
||||||
|
alt((
|
||||||
|
line_comment,
|
||||||
|
block_comment,
|
||||||
|
take_while(1.., (AsChar::is_space, '\n', '\r')),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_comment<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
("//", take_while(0.., |c| c != '\n'))
|
||||||
|
.take()
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_comment<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
("/*", cut_err(terminated(take_until(0.., "*/"), "*/")))
|
||||||
|
.take()
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches a sass interpolation of the form #{...}
|
||||||
|
fn sass_interpolation<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
(
|
||||||
|
"#{",
|
||||||
|
cut_err(terminated(take_till(1.., ('{', '}', '\n')), '}')),
|
||||||
|
)
|
||||||
|
.take()
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identifier<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
(
|
||||||
|
one_of(('_', '-', AsChar::is_alpha)),
|
||||||
|
take_while(0.., ('_', '-', AsChar::is_alphanum)),
|
||||||
|
)
|
||||||
|
.take()
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn class<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
preceded('.', identifier).parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn global<'s>(input: &mut &'s str) -> ModalResult<Global<'s>> {
|
||||||
|
let (inner, outer) = preceded(
|
||||||
|
":global(",
|
||||||
|
cut_err(terminated(
|
||||||
|
stuff_till(0.., (')', '(', '{')), // inner
|
||||||
|
')',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_taken() // outer
|
||||||
|
.parse_next(input)?;
|
||||||
|
Ok(Global { inner, outer })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_dq<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
let str_char = alt((none_of(['"']).void(), "\\\"".void()));
|
||||||
|
let str_chars = recognize_repeat(0.., str_char);
|
||||||
|
|
||||||
|
preceded('"', cut_err(terminated(str_chars, '"'))).parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_sq<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
let str_char = alt((none_of(['\'']).void(), "\\'".void()));
|
||||||
|
let str_chars = recognize_repeat(0.., str_char);
|
||||||
|
|
||||||
|
preceded('\'', cut_err(terminated(str_chars, '\''))).parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
alt((string_dq, string_sq)).parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Behaves like take_till except it finds and parses strings and
|
||||||
|
/// comments (allowing those to contain the end condition characters).
|
||||||
|
fn stuff_till<'s>(
|
||||||
|
range: impl Into<Range>,
|
||||||
|
list: impl ContainsToken<char>,
|
||||||
|
) -> impl Parser<&'s str, &'s str, ErrMode<ContextError>> {
|
||||||
|
recognize_repeat(
|
||||||
|
range,
|
||||||
|
alt((
|
||||||
|
string.void(),
|
||||||
|
block_comment.void(),
|
||||||
|
line_comment.void(),
|
||||||
|
sass_interpolation.void(),
|
||||||
|
'/'.void(),
|
||||||
|
'#'.void(),
|
||||||
|
take_till(1.., ('\'', '"', '/', '#', list)).void(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selector<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
|
||||||
|
repeat(
|
||||||
|
1..,
|
||||||
|
alt((
|
||||||
|
class.map(|c| Some(CssFragment::Class(c))),
|
||||||
|
global.map(|g| Some(CssFragment::Global(g))),
|
||||||
|
':'.map(|_| None),
|
||||||
|
stuff_till(1.., ('.', ';', '{', '}', ':')).map(|_| None),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.fold(Vec::new, |mut acc, item| {
|
||||||
|
if let Some(item) = item {
|
||||||
|
acc.push(item);
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn declaration<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
(
|
||||||
|
(opt('$'), identifier),
|
||||||
|
ws,
|
||||||
|
':',
|
||||||
|
terminated(
|
||||||
|
stuff_till(1.., (';', '{', '}')),
|
||||||
|
alt((';', peek('}'))), // semicolon is optional if it's the last element in a rule block
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.take()
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_rule_block_statement<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
|
||||||
|
let content = alt((
|
||||||
|
declaration.map(|_| Vec::new()), //
|
||||||
|
at_rule,
|
||||||
|
style_rule,
|
||||||
|
));
|
||||||
|
delimited(ws, content, ws).parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_rule_block_contents<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
|
||||||
|
repeat(0.., style_rule_block_statement)
|
||||||
|
.fold(Vec::new, |mut acc, mut item| {
|
||||||
|
acc.append(&mut item);
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_rule_block<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
|
||||||
|
preceded(
|
||||||
|
'{',
|
||||||
|
cut_err(terminated(style_rule_block_contents, (ws, '}'))),
|
||||||
|
)
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_rule<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
|
||||||
|
let (mut classes, mut nested_classes) = (selector, style_rule_block).parse_next(input)?;
|
||||||
|
classes.append(&mut nested_classes);
|
||||||
|
Ok(classes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn at_rule<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
|
||||||
|
let (identifier, char) = preceded(
|
||||||
|
'@',
|
||||||
|
cut_err((
|
||||||
|
terminated(identifier, stuff_till(0.., ('{', '}', ';'))),
|
||||||
|
alt(('{', ';', peek('}'))),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.parse_next(input)?;
|
||||||
|
|
||||||
|
if char != '{' {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
match identifier {
|
||||||
|
"media" | "layer" | "container" | "include" => {
|
||||||
|
cut_err(terminated(style_rule_block_contents, '}')).parse_next(input)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
cut_err(terminated(unknown_block_contents, '}')).parse_next(input)?;
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknown_block_contents<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||||
|
recognize_repeat(
|
||||||
|
0..,
|
||||||
|
alt((
|
||||||
|
stuff_till(1.., ('{', '}')).void(),
|
||||||
|
('{', cut_err((unknown_block_contents, '}'))).void(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************************************************//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_class() {
|
||||||
|
let mut input = "._x1a2b Hello";
|
||||||
|
|
||||||
|
let r = class.parse_next(&mut input);
|
||||||
|
assert_eq!(r, Ok("_x1a2b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_selector() {
|
||||||
|
let mut input = ".foo.bar [value=\"fa.sdasd\"] /* .banana */ // .apple \n \t .cry {";
|
||||||
|
|
||||||
|
let r = selector.parse_next(&mut input);
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Ok(vec![
|
||||||
|
CssFragment::Class("foo"),
|
||||||
|
CssFragment::Class("bar"),
|
||||||
|
CssFragment::Class("cry")
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut input = "{";
|
||||||
|
|
||||||
|
let r = selector.take().parse_next(&mut input);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_declaration() {
|
||||||
|
let mut input = "background-color \t : red;";
|
||||||
|
|
||||||
|
let r = declaration.parse_next(&mut input);
|
||||||
|
assert_eq!(r, Ok("background-color \t : red;"));
|
||||||
|
|
||||||
|
let r = declaration.parse_next(&mut input);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_rule() {
|
||||||
|
let mut input = ".foo.bar {
|
||||||
|
background-color: red;
|
||||||
|
.baz {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
$some-scss-var: 10px;
|
||||||
|
@some-at-rule blah blah;
|
||||||
|
@media blah .blah {
|
||||||
|
.moo {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container (width > 700px) {
|
||||||
|
.zoo {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}END";
|
||||||
|
|
||||||
|
let r = style_rule.parse_next(&mut input);
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Ok(vec![
|
||||||
|
CssFragment::Class("foo"),
|
||||||
|
CssFragment::Class("bar"),
|
||||||
|
CssFragment::Class("baz"),
|
||||||
|
CssFragment::Class("moo"),
|
||||||
|
CssFragment::Class("zoo")
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(input, "END");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_at_rule_simple() {
|
||||||
|
let mut input = "@simple-rule blah \"asd;asd\" blah;";
|
||||||
|
|
||||||
|
let r = at_rule.parse_next(&mut input);
|
||||||
|
assert_eq!(r, Ok(vec![]));
|
||||||
|
|
||||||
|
assert!(input.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_at_rule_unknown() {
|
||||||
|
let mut input = "@unknown blah \"asdasd\" blah {
|
||||||
|
bunch of stuff {
|
||||||
|
// things inside {
|
||||||
|
blah
|
||||||
|
' { '
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
color: blue;
|
||||||
|
|
||||||
|
.baz {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
let r = at_rule.parse_next(&mut input);
|
||||||
|
assert_eq!(r, Ok(vec![]));
|
||||||
|
|
||||||
|
assert!(input.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_at_rule_media() {
|
||||||
|
let mut input = "@media blah \"asdasd\" blah {
|
||||||
|
.foo {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
color: blue;
|
||||||
|
|
||||||
|
.baz {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
let r = at_rule.parse_next(&mut input);
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Ok(vec![
|
||||||
|
CssFragment::Class("foo"),
|
||||||
|
CssFragment::Class("bar"),
|
||||||
|
CssFragment::Class("baz")
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(input.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_at_rule_layer() {
|
||||||
|
let mut input = "@layer test {
|
||||||
|
.foo {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
color: blue;
|
||||||
|
|
||||||
|
.baz {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
let r = at_rule.parse_next(&mut input);
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Ok(vec![
|
||||||
|
CssFragment::Class("foo"),
|
||||||
|
CssFragment::Class("bar"),
|
||||||
|
CssFragment::Class("baz")
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(input.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_top_level() {
|
||||||
|
let mut input = "// tool.module.scss
|
||||||
|
|
||||||
|
.default_border {
|
||||||
|
border-color: lch(100% 10 10);
|
||||||
|
border-style: dashed double;
|
||||||
|
border-radius: 30px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media testing {
|
||||||
|
.media-foo {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer {
|
||||||
|
.layer-foo {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mixin {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.include-foo {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer foo;
|
||||||
|
|
||||||
|
@debug 1+2 * 3==1+(2 * 3); // true
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1em;
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: lch(100% 10 10);
|
||||||
|
border-style: dashed double;
|
||||||
|
border-radius: 30px;
|
||||||
|
margin: 1em;
|
||||||
|
background-color: lch(45% 9.5 140.4);
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@debug 1+2 * 3==1+(2 * 3); // true
|
||||||
|
";
|
||||||
|
|
||||||
|
let r = style_rule_block_contents.parse_next(&mut input);
|
||||||
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
Ok(vec![
|
||||||
|
CssFragment::Class("default_border"),
|
||||||
|
CssFragment::Class("media-foo"),
|
||||||
|
CssFragment::Class("layer-foo"),
|
||||||
|
CssFragment::Class("include-foo"),
|
||||||
|
CssFragment::Class("container"),
|
||||||
|
CssFragment::Class("bar"),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("{input}");
|
||||||
|
assert!(input.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sass_interpolation() {
|
||||||
|
let mut input = "#{$test-test}END";
|
||||||
|
|
||||||
|
let r = sass_interpolation.parse_next(&mut input);
|
||||||
|
assert_eq!(r, Ok("#{$test-test}"));
|
||||||
|
|
||||||
|
assert_eq!(input, "END");
|
||||||
|
|
||||||
|
let mut input = "#{$test-test
|
||||||
|
}END";
|
||||||
|
let r = sass_interpolation.parse_next(&mut input);
|
||||||
|
assert!(r.is_err());
|
||||||
|
|
||||||
|
let mut input = "#{$test-test";
|
||||||
|
let r = sass_interpolation.parse_next(&mut input);
|
||||||
|
assert!(r.is_err());
|
||||||
|
|
||||||
|
let mut input = "#{$test-te{st}";
|
||||||
|
let r = sass_interpolation.parse_next(&mut input);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_class_mappings() {
|
||||||
|
let css = r#".foo.bar {
|
||||||
|
background-color: red;
|
||||||
|
:global(.baz) {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
:global(.bag .biz) {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
.zig {
|
||||||
|
|
||||||
|
}
|
||||||
|
.bong {}
|
||||||
|
.zig {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let hash = "abc1234";
|
||||||
|
let mappings = get_class_mappings(css, hash).unwrap();
|
||||||
|
let expected = [
|
||||||
|
("bag", "bag"),
|
||||||
|
("bar", "bar-abc1234"),
|
||||||
|
("baz", "baz"),
|
||||||
|
("biz", "biz"),
|
||||||
|
("bong", "bong-abc1234"),
|
||||||
|
("foo", "foo-abc1234"),
|
||||||
|
("zig", "zig-abc1234"),
|
||||||
|
];
|
||||||
|
if mappings.len() != expected.len() {
|
||||||
|
panic!(
|
||||||
|
"Expected {} mappings, got {}",
|
||||||
|
expected.len(),
|
||||||
|
mappings.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (i, (original, hashed)) in mappings.iter().enumerate() {
|
||||||
|
assert_eq!(expected[i].0, *original);
|
||||||
|
assert_eq!(expected[i].1, *hashed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser_error_on_nested_globals() {
|
||||||
|
let css = r#".foo :global(.bar .baz) {
|
||||||
|
color: blue;
|
||||||
|
}"#;
|
||||||
|
let result = parse_css(css);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let css = r#".foo :global(.bar :global(.baz)) {
|
||||||
|
color: blue;
|
||||||
|
}"#;
|
||||||
|
let result = parse_css(css);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_resolve_global_inner_classes_nested() {
|
||||||
|
let global = Global {
|
||||||
|
inner: ".foo :global(.bar)",
|
||||||
|
outer: ":global(.foo :global(.bar))",
|
||||||
|
};
|
||||||
|
let _ = resolve_global_inner_classes(global);
|
||||||
|
}
|
||||||
@@ -18,3 +18,6 @@ pub use asset::*;
|
|||||||
|
|
||||||
mod css_module;
|
mod css_module;
|
||||||
pub use css_module::*;
|
pub use css_module::*;
|
||||||
|
|
||||||
|
mod css_module_parser;
|
||||||
|
pub use css_module_parser::*;
|
||||||
@@ -1,31 +1,21 @@
|
|||||||
use crate::{asset::AssetParser, resolve_path};
|
use crate::{asset::AssetParser, resolve_path};
|
||||||
use macro_string::MacroString;
|
use macro_string::MacroString;
|
||||||
|
use manganis_core::{create_module_hash, get_class_mappings};
|
||||||
use proc_macro2::{Span, TokenStream};
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
token::Comma,
|
token::Comma,
|
||||||
Ident, Token, Visibility,
|
Ident, ItemStruct,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct CssModuleParser {
|
pub(crate) struct CssModuleAttribute {
|
||||||
/// Whether the ident is const or static.
|
|
||||||
styles_vis: Visibility,
|
|
||||||
styles_ident: Ident,
|
|
||||||
asset_parser: AssetParser,
|
asset_parser: AssetParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for CssModuleParser {
|
impl Parse for CssModuleAttribute {
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
// NEW: macro!(pub? STYLES_IDENT = "/path.css");
|
|
||||||
// pub(x)?
|
|
||||||
let styles_vis = input.parse::<Visibility>()?;
|
|
||||||
|
|
||||||
// Styles Ident
|
|
||||||
let styles_ident = input.parse::<Ident>()?;
|
|
||||||
let _equals = input.parse::<Token![=]>()?;
|
|
||||||
|
|
||||||
// Asset path "/path.css"
|
// Asset path "/path.css"
|
||||||
let (MacroString(src), path_expr) = input.call(crate::parse_with_tokens)?;
|
let (MacroString(src), path_expr) = input.call(crate::parse_with_tokens)?;
|
||||||
let asset = resolve_path(&src, path_expr.span());
|
let asset = resolve_path(&src, path_expr.span());
|
||||||
@@ -44,25 +34,46 @@ impl Parse for CssModuleParser {
|
|||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { asset_parser })
|
||||||
styles_vis,
|
|
||||||
styles_ident,
|
|
||||||
asset_parser,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for CssModuleParser {
|
pub(crate) fn expand_css_module_struct(
|
||||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
tokens: &mut proc_macro2::TokenStream,
|
||||||
|
attribute: &CssModuleAttribute,
|
||||||
|
item_struct: &ItemStruct,
|
||||||
|
) {
|
||||||
|
if !item_struct.fields.is_empty() {
|
||||||
|
let err = syn::Error::new(
|
||||||
|
item_struct.fields.span(),
|
||||||
|
"css_module can only be applied to unit structs",
|
||||||
|
)
|
||||||
|
.into_compile_error();
|
||||||
|
tokens.append_all(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !item_struct.generics.params.is_empty() {
|
||||||
|
let err = syn::Error::new(
|
||||||
|
item_struct.generics.span(),
|
||||||
|
"css_module cannot be applied to generic structs",
|
||||||
|
)
|
||||||
|
.into_compile_error();
|
||||||
|
tokens.append_all(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let struct_vis = &item_struct.vis;
|
||||||
|
let struct_name = &item_struct.ident;
|
||||||
|
let struct_name_private = format_ident!("__{}", struct_name);
|
||||||
|
|
||||||
// Use the regular asset parser to generate the linker bridge.
|
// Use the regular asset parser to generate the linker bridge.
|
||||||
let mut linker_tokens = quote! {
|
let mut linker_tokens = quote! {
|
||||||
/// Auto-generated Manganis asset for css modules.
|
/// Auto-generated Manganis asset for css modules.
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
const ASSET: manganis::Asset =
|
const ASSET: manganis::Asset =
|
||||||
};
|
};
|
||||||
self.asset_parser.to_tokens(&mut linker_tokens);
|
attribute.asset_parser.to_tokens(&mut linker_tokens);
|
||||||
|
|
||||||
let path = match self.asset_parser.asset.as_ref() {
|
let asset = match attribute.asset_parser.asset.as_ref() {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err = err.to_string();
|
let err = err.to_string();
|
||||||
@@ -71,69 +82,34 @@ impl ToTokens for CssModuleParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the file hash
|
let css = std::fs::read_to_string(asset).expect("Unable to read css module file");
|
||||||
let hash = match crate::hash_file_contents(path) {
|
|
||||||
Ok(hash) => hash,
|
|
||||||
Err(err) => {
|
|
||||||
let err = err.to_string();
|
|
||||||
tokens.append_all(quote! { compile_error!(#err) });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process css idents
|
|
||||||
let css = std::fs::read_to_string(path).unwrap();
|
|
||||||
let (classes, ids) = manganis_core::collect_css_idents(&css);
|
|
||||||
|
|
||||||
let mut values = Vec::new();
|
let mut values = Vec::new();
|
||||||
|
|
||||||
// Create unique module name based on styles ident.
|
let hash = create_module_hash(asset);
|
||||||
let styles_ident = &self.styles_ident;
|
let class_mappings = get_class_mappings(css.as_str(), hash.as_str()).expect("Invalid css");
|
||||||
let mod_name = format_ident!("__{}_module", styles_ident);
|
|
||||||
|
|
||||||
// Generate id struct field tokens.
|
|
||||||
for id in ids.iter() {
|
|
||||||
let as_snake = to_snake_case(id);
|
|
||||||
let ident = Ident::new(&as_snake, Span::call_site());
|
|
||||||
|
|
||||||
values.push(quote! {
|
|
||||||
pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::const_serialize::ConstStr::new(#id).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate class struct field tokens.
|
// Generate class struct field tokens.
|
||||||
for class in classes.iter() {
|
for (old_class, new_class) in class_mappings.iter() {
|
||||||
let as_snake = to_snake_case(class);
|
let as_snake = to_snake_case(old_class);
|
||||||
let as_snake = match ids.contains(class) {
|
|
||||||
false => as_snake,
|
|
||||||
true => format!("{as_snake}_class"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ident = Ident::new(&as_snake, Span::call_site());
|
let ident = Ident::new(&as_snake, Span::call_site());
|
||||||
values.push(quote! {
|
values.push(quote! {
|
||||||
pub const #ident: #mod_name::__CssIdent = #mod_name::__CssIdent { inner: manganis::macro_helpers::const_serialize::ConstStr::new(#class).push_str(#mod_name::__ASSET_HASH.as_str()).as_str() };
|
pub const #ident: #struct_name_private::__CssIdent = #struct_name_private::__CssIdent { inner: #new_class };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = &self.asset_parser.options;
|
|
||||||
let styles_vis = &self.styles_vis;
|
|
||||||
|
|
||||||
// We use a PhantomData to prevent Rust from complaining about an unused lifetime if a css module without any idents is used.
|
// We use a PhantomData to prevent Rust from complaining about an unused lifetime if a css module without any idents is used.
|
||||||
tokens.extend(quote! {
|
tokens.extend(quote! {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[allow(missing_docs, non_snake_case)]
|
#[allow(missing_docs, non_snake_case)]
|
||||||
mod #mod_name {
|
mod #struct_name_private {
|
||||||
#[allow(unused_imports)]
|
use dioxus::prelude::*;
|
||||||
use manganis::{self, CssModuleAssetOptions};
|
|
||||||
|
|
||||||
#linker_tokens;
|
#linker_tokens;
|
||||||
|
|
||||||
// Get the hash to use when builidng hashed css idents.
|
|
||||||
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
|
|
||||||
pub(super) const __ASSET_HASH: manganis::macro_helpers::const_serialize::ConstStr = manganis::macro_helpers::hash_asset(&__ASSET_OPTIONS, #hash);
|
|
||||||
|
|
||||||
// Css ident class for deref stylesheet inclusion.
|
// Css ident class for deref stylesheet inclusion.
|
||||||
pub(super) struct __CssIdent { pub inner: &'static str }
|
pub struct __CssIdent { pub inner: &'static str }
|
||||||
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
@@ -167,14 +143,19 @@ impl ToTokens for CssModuleParser {
|
|||||||
|
|
||||||
/// Auto-generated idents struct for CSS modules.
|
/// Auto-generated idents struct for CSS modules.
|
||||||
#[allow(missing_docs, non_snake_case)]
|
#[allow(missing_docs, non_snake_case)]
|
||||||
#styles_vis struct #styles_ident {}
|
#struct_vis struct #struct_name {}
|
||||||
|
|
||||||
impl #styles_ident {
|
impl #struct_name {
|
||||||
#( #values )*
|
#( #values )*
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
impl dioxus::core::IntoAttributeValue for #struct_name_private::__CssIdent {
|
||||||
|
fn into_value(self) -> dioxus::core::AttributeValue {
|
||||||
|
dioxus::core::AttributeValue::Text(self.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert camel and kebab case to snake case.
|
/// Convert camel and kebab case to snake case.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
hash::Hasher,
|
|
||||||
io::Read,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use css_module::CssModuleParser;
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
parse_macro_input,
|
parse_macro_input, ItemStruct,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod asset;
|
pub(crate) mod asset;
|
||||||
@@ -22,6 +17,8 @@ pub(crate) mod linker;
|
|||||||
|
|
||||||
use linker::generate_link_section;
|
use linker::generate_link_section;
|
||||||
|
|
||||||
|
use crate::css_module::{expand_css_module_struct, CssModuleAttribute};
|
||||||
|
|
||||||
/// The asset macro collects assets that will be included in the final binary
|
/// The asset macro collects assets that will be included in the final binary
|
||||||
///
|
///
|
||||||
/// # Files
|
/// # Files
|
||||||
@@ -84,108 +81,149 @@ pub fn option_asset(input: TokenStream) -> TokenStream {
|
|||||||
asset.expand_option_tokens().into()
|
asset.expand_option_tokens().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate type-safe and globally-unique CSS identifiers from a CSS module.
|
/// Generate type-safe styles with scoped CSS class names.
|
||||||
///
|
///
|
||||||
/// CSS modules allow you to have unique, scoped and type-safe CSS identifiers. A CSS module is a CSS file with the `.module.css` file extension.
|
/// The `css_module` attribute macro creates scoped CSS modules that prevent class name collisions
|
||||||
/// The `css_module!()` macro allows you to utilize CSS modules in your Rust projects.
|
/// by making each class globally unique. It expands the annotated struct to provide type-safe
|
||||||
|
/// identifiers for your CSS classes, allowing you to reference styles in your Rust code with
|
||||||
|
/// compile-time guarantees.
|
||||||
///
|
///
|
||||||
/// # Syntax
|
/// # Syntax
|
||||||
///
|
///
|
||||||
/// The `css_module!()` macro takes a few items.
|
/// The `css_module` attribute takes:
|
||||||
/// - A styles struct identifier. This is the `struct` you use to access your type-safe CSS identifiers in Rust.
|
/// - The asset string path - the absolute path (from the crate root) to your CSS file.
|
||||||
/// - The asset string path. This is the absolute path (from the crate root) to your CSS module.
|
/// - Optional `AssetOptions` to configure the processing of your CSS module.
|
||||||
/// - An optional `CssModuleAssetOptions` struct to configure the processing of your CSS module.
|
|
||||||
///
|
///
|
||||||
|
/// It must be applied to a unit struct:
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// css_module!(StylesIdent = "/my.module.css", AssetOptions::css_module());
|
/// #[css_module("/assets/my-styles.css")]
|
||||||
/// ```
|
/// struct Styles;
|
||||||
///
|
///
|
||||||
/// The styles struct can be made public by appending `pub` before the identifier.
|
/// #[css_module("/assets/my-styles.css", AssetOptions::css_module().with_minify(true))]
|
||||||
/// Read the [Variable Visibility](#variable-visibility) section for more information.
|
/// struct Styles;
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Generation
|
/// # Generation
|
||||||
///
|
///
|
||||||
/// The `css_module!()` macro does two few things:
|
/// The `css_module` attribute macro does two things:
|
||||||
/// - It generates an asset using the `asset!()` macro and automatically inserts it into the app meta.
|
/// - It generates an asset and automatically inserts it as a stylesheet link in the document.
|
||||||
/// - It generates a struct with snake-case associated constants of your CSS idents.
|
/// - It expands the annotated struct with snake-case associated constants for your CSS class names.
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// // This macro usage:
|
/// // This macro usage:
|
||||||
/// css_module!(Styles = "/mycss.module.css");
|
/// #[css_module("/assets/mycss.css")]
|
||||||
|
/// struct Styles;
|
||||||
///
|
///
|
||||||
/// // Will generate this (simplified):
|
/// // Will expand the struct to (simplified):
|
||||||
/// struct Styles {}
|
/// struct Styles {}
|
||||||
///
|
///
|
||||||
/// impl Styles {
|
/// impl Styles {
|
||||||
/// // This can be accessed with `Styles::your_ident`
|
/// // Snake-cased class names can be accessed like this:
|
||||||
/// pub const your_ident: &str = "abc";
|
/// pub const your_class: &str = "your_class-a1b2c3";
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # CSS Identifier Collection
|
/// # CSS Class Name Scoping
|
||||||
/// The macro will collect all identifiers used in your CSS module, convert them into snake_case, and generate a struct and fields around those identifier names.
|
|
||||||
///
|
///
|
||||||
/// For example, `#fooBar` will become `foo_bar`.
|
/// **The macro only processes CSS class selectors (`.class-name`).** Other selectors like IDs (`#id`),
|
||||||
|
/// element selectors (`div`, `p`), attribute selectors, etc. are left unchanged and not exposed as
|
||||||
|
/// Rust constants.
|
||||||
///
|
///
|
||||||
/// Identifier used only inside of a media query, will not be collected (not yet supported). To get around this, you can use an empty block for the identifier:
|
/// The macro collects all class selectors in your CSS file and transforms them to be globally unique
|
||||||
|
/// by appending a hash. For example, `.myClass` becomes `.myClass-a1b2c3` where `a1b2c3` is a hash
|
||||||
|
/// of the file path.
|
||||||
|
///
|
||||||
|
/// Class names are converted to snake_case for the Rust constants. For example:
|
||||||
|
/// - `.fooBar` becomes `Styles::foo_bar`
|
||||||
|
/// - `.my-class` becomes `Styles::my_class`
|
||||||
|
///
|
||||||
|
/// To prevent a class from being scoped, wrap it in `:global()`:
|
||||||
/// ```css
|
/// ```css
|
||||||
/// /* Empty ident block to ensure collection */
|
/// /* This class will be scoped */
|
||||||
/// #foo {}
|
/// .my-class { color: blue; }
|
||||||
///
|
///
|
||||||
/// @media ... {
|
/// /* This class will NOT be scoped (no hash added) */
|
||||||
/// #foo { ... }
|
/// :global(.global-class) { color: red; }
|
||||||
/// }
|
///
|
||||||
|
/// /* Element selectors and other CSS remain unchanged */
|
||||||
|
/// div { margin: 0; }
|
||||||
|
/// #my-id { padding: 10px; }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Variable Visibility
|
/// # Using Multiple CSS Modules
|
||||||
/// If you want your asset or styles constant to be public, you can add the `pub` keyword in front of them.
|
///
|
||||||
/// Restricted visibility (`pub(super)`, `pub(crate)`, etc) is also supported.
|
/// Multiple `css_module` attributes can be used in the same scope by applying them to different structs:
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// css_module!(pub Styles = "/mycss.module.css");
|
/// // First CSS module
|
||||||
|
/// #[css_module("/assets/styles1.css")]
|
||||||
|
/// struct Styles;
|
||||||
|
///
|
||||||
|
/// // Second CSS module with a different struct name
|
||||||
|
/// #[css_module("/assets/styles2.css")]
|
||||||
|
/// struct OtherStyles;
|
||||||
|
///
|
||||||
|
/// // Access classes from both:
|
||||||
|
/// rsx! {
|
||||||
|
/// div { class: Styles::container }
|
||||||
|
/// div { class: OtherStyles::button }
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Asset Options
|
/// # Asset Options
|
||||||
/// Similar to the `asset!()` macro, you can pass an optional `CssModuleAssetOptions` to configure a few processing settings.
|
|
||||||
/// ```rust, ignore
|
|
||||||
/// use manganis::CssModuleAssetOptions;
|
|
||||||
///
|
///
|
||||||
/// css_module!(Styles = "/mycss.module.css",
|
/// Similar to the `asset!()` macro, you can pass optional `AssetOptions` to configure processing:
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// #[css_module(
|
||||||
|
/// "/assets/mycss.css",
|
||||||
/// AssetOptions::css_module()
|
/// AssetOptions::css_module()
|
||||||
/// .with_minify(true)
|
/// .with_minify(true)
|
||||||
/// .with_preload(false),
|
/// .with_preload(false)
|
||||||
/// );
|
/// )]
|
||||||
|
/// struct Styles;
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Example
|
||||||
/// First you need a CSS module:
|
///
|
||||||
|
/// First create a CSS file:
|
||||||
/// ```css
|
/// ```css
|
||||||
/// /* mycss.module.css */
|
/// /* assets/styles.css */
|
||||||
///
|
///
|
||||||
/// #header {
|
/// .container {
|
||||||
/// padding: 50px;
|
/// padding: 20px;
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// .header {
|
|
||||||
/// margin: 20px;
|
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// .button {
|
/// .button {
|
||||||
/// background-color: #373737;
|
/// background-color: #373737;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
|
||||||
/// Then you can use the `css_module!()` macro in your Rust project:
|
|
||||||
/// ```rust, ignore
|
|
||||||
/// css_module!(Styles = "/mycss.module.css");
|
|
||||||
///
|
///
|
||||||
/// println!("{}", Styles::header);
|
/// :global(.global-text) {
|
||||||
/// println!("{}", Styles::header_class);
|
/// font-weight: bold;
|
||||||
/// println!("{}", Styles::button);
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[proc_macro]
|
///
|
||||||
#[doc(hidden)]
|
/// Then use the `css_module` attribute:
|
||||||
pub fn css_module(input: TokenStream) -> TokenStream {
|
/// ```rust, ignore
|
||||||
let style = parse_macro_input!(input as CssModuleParser);
|
/// use dioxus::prelude::*;
|
||||||
quote! { #style }.into_token_stream().into()
|
///
|
||||||
|
/// fn app() -> Element {
|
||||||
|
/// #[css_module("/assets/styles.css")]
|
||||||
|
/// struct Styles;
|
||||||
|
///
|
||||||
|
/// rsx! {
|
||||||
|
/// div { class: Styles::container,
|
||||||
|
/// button { class: Styles::button, "Click me" }
|
||||||
|
/// span { class: Styles::global_text, "This uses global class" }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn css_module(input: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let attribute = parse_macro_input!(input as CssModuleAttribute);
|
||||||
|
let item_struct = parse_macro_input!(item as ItemStruct);
|
||||||
|
let mut tokens = proc_macro2::TokenStream::new();
|
||||||
|
expand_css_module_struct(&mut tokens, &attribute, &item_struct);
|
||||||
|
tokens.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
|
fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
|
||||||
@@ -251,46 +289,6 @@ fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_file_contents(file_path: &Path) -> Result<u64, AssetParseError> {
|
|
||||||
// Create a hasher
|
|
||||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
|
||||||
|
|
||||||
// If this is a folder, hash the folder contents
|
|
||||||
if file_path.is_dir() {
|
|
||||||
let files = std::fs::read_dir(file_path).map_err(|err| AssetParseError::IoError {
|
|
||||||
err,
|
|
||||||
path: file_path.to_path_buf(),
|
|
||||||
})?;
|
|
||||||
for file in files.flatten() {
|
|
||||||
let path = file.path();
|
|
||||||
hash_file_contents(&path)?;
|
|
||||||
}
|
|
||||||
return Ok(hash.finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, open the file to get its contents
|
|
||||||
let mut file = std::fs::File::open(file_path).map_err(|err| AssetParseError::IoError {
|
|
||||||
err,
|
|
||||||
path: file_path.to_path_buf(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
|
|
||||||
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
|
|
||||||
// know the file contents, so we only include that hash
|
|
||||||
let mut buffer = [0; 8192];
|
|
||||||
loop {
|
|
||||||
let read = file
|
|
||||||
.read(&mut buffer)
|
|
||||||
.map_err(AssetParseError::FailedToReadAsset)?;
|
|
||||||
if read == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
hash.write(&buffer[..read]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(hash.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse `T`, while also collecting the tokens it was parsed from.
|
/// Parse `T`, while also collecting the tokens it was parsed from.
|
||||||
fn parse_with_tokens<T: Parse>(input: ParseStream) -> syn::Result<(T, proc_macro2::TokenStream)> {
|
fn parse_with_tokens<T: Parse>(input: ParseStream) -> syn::Result<(T, proc_macro2::TokenStream)> {
|
||||||
let begin = input.cursor();
|
let begin = input.cursor();
|
||||||
@@ -311,9 +309,7 @@ fn parse_with_tokens<T: Parse>(input: ParseStream) -> syn::Result<(T, proc_macro
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum AssetParseError {
|
enum AssetParseError {
|
||||||
AssetDoesntExist { path: PathBuf },
|
AssetDoesntExist { path: PathBuf },
|
||||||
IoError { err: std::io::Error, path: PathBuf },
|
|
||||||
InvalidPath { path: PathBuf },
|
InvalidPath { path: PathBuf },
|
||||||
FailedToReadAsset(std::io::Error),
|
|
||||||
RelativeAssetPath,
|
RelativeAssetPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,9 +319,6 @@ impl std::fmt::Display for AssetParseError {
|
|||||||
AssetParseError::AssetDoesntExist { path } => {
|
AssetParseError::AssetDoesntExist { path } => {
|
||||||
write!(f, "Asset at {} doesn't exist", path.display())
|
write!(f, "Asset at {} doesn't exist", path.display())
|
||||||
}
|
}
|
||||||
AssetParseError::IoError { path, err } => {
|
|
||||||
write!(f, "Failed to read file: {}; {}", path.display(), err)
|
|
||||||
}
|
|
||||||
AssetParseError::InvalidPath { path } => {
|
AssetParseError::InvalidPath { path } => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@@ -333,7 +326,6 @@ impl std::fmt::Display for AssetParseError {
|
|||||||
path.display()
|
path.display()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AssetParseError::FailedToReadAsset(err) => write!(f, "Failed to read asset: {}", err),
|
|
||||||
AssetParseError::RelativeAssetPath => write!(f, "Failed to resolve relative asset path. Relative assets are only supported in rust 1.88+."),
|
AssetParseError::RelativeAssetPath => write!(f, "Failed to resolve relative asset path. Relative assets are only supported in rust 1.88+."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod macro_helpers;
|
pub mod macro_helpers;
|
||||||
pub use manganis_macro::asset;
|
pub use manganis_macro::asset;
|
||||||
pub use manganis_macro::option_asset;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use manganis_macro::css_module;
|
pub use manganis_macro::css_module;
|
||||||
|
pub use manganis_macro::option_asset;
|
||||||
|
|
||||||
pub use manganis_core::{
|
pub use manganis_core::{
|
||||||
Asset, AssetOptions, AssetVariant, BundledAsset, CssAssetOptions, CssModuleAssetOptions,
|
Asset, AssetOptions, AssetVariant, BundledAsset, CssAssetOptions, CssModuleAssetOptions,
|
||||||
|
|||||||
Reference in new issue
Block a user