fix!: bring const scopes back
Some checks failed
cargo basic build-test / Sabry - latest (push) Has been cancelled
cargo basic build-test / Sabry - latset - examples (push) Has been cancelled
cargo basic build-test / Sabry - latset - examples-1 (push) Has been cancelled
cargo basic build-test / Sabry - latset - examples-2 (push) Has been cancelled
Some checks failed
cargo basic build-test / Sabry - latest (push) Has been cancelled
cargo basic build-test / Sabry - latset - examples (push) Has been cancelled
cargo basic build-test / Sabry - latset - examples-1 (push) Has been cancelled
cargo basic build-test / Sabry - latset - examples-2 (push) Has been cancelled
- replace grass with sasso - drop lightningcss - rearrange crate and workspace hierarchy & features
This commit is contained in:
parent
44d43e1e65
commit
4e5cc9cb8b
26 changed files with 2614 additions and 768 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,7 +3,6 @@ rust-toolchain.toml
|
||||||
.vscode
|
.vscode
|
||||||
.profile
|
.profile
|
||||||
.envrc
|
.envrc
|
||||||
/Cargo.lock
|
|
||||||
/tests/sabry_intermediate
|
/tests/sabry_intermediate
|
||||||
/tests/sabry_output
|
/tests/sabry_output
|
||||||
/playground
|
/playground
|
||||||
|
|
|
||||||
2030
Cargo.lock
generated
Normal file
2030
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
62
Cargo.toml
62
Cargo.toml
|
|
@ -1,11 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = ["sabry", "sabry_procmacro", "sabry_procmacro_impl", "sabry_intrnl", "sabry_build"]
|
||||||
"sabry_procmacro",
|
|
||||||
"sabry_procmacro_impl",
|
|
||||||
"sabry_intrnl",
|
|
||||||
"sabry_build",
|
|
||||||
]
|
|
||||||
exclude = ["examples"]
|
exclude = ["examples"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
@ -19,15 +14,21 @@ publish = true
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
## Workspace
|
## Workspace
|
||||||
sabry_intrnl = { path = "sabry_intrnl", version = "0.0.7" }
|
sabry.path = "sabry"
|
||||||
sabry_procmacro = { path = "sabry_procmacro", version = "0.0.7" }
|
sabry.version = "0.0.7"
|
||||||
sabry_procmacro_impl = { path = "sabry_procmacro_impl", version = "0.0.7" }
|
sabry_intrnl.path = "sabry_intrnl"
|
||||||
sabry_build = { path = "sabry_build", version = "0.0.7" }
|
sabry_intrnl.version = "0.0.7"
|
||||||
|
sabry_procmacro.path = "sabry_procmacro"
|
||||||
|
sabry_procmacro.version = "0.0.7"
|
||||||
|
sabry_procmacro_impl.path = "sabry_procmacro_impl"
|
||||||
|
sabry_procmacro_impl.version = "0.0.7"
|
||||||
|
sabry_build.path = "sabry_build"
|
||||||
|
sabry_build.version = "0.0.7"
|
||||||
## CSS
|
## CSS
|
||||||
lightningcss.version = "1.0.0-alpha.72"
|
lightningcss.version = "1.0.0-alpha.72"
|
||||||
lightningcss.default-features = false
|
lightningcss.default-features = false
|
||||||
grass.version = "^0.13"
|
sasso.version = "^0.21"
|
||||||
grass.default-features = false
|
sasso.default-features = false
|
||||||
raffia.version = "^0.13"
|
raffia.version = "^0.13"
|
||||||
## Marshall
|
## Marshall
|
||||||
serde.version = "^1"
|
serde.version = "^1"
|
||||||
|
|
@ -43,38 +44,5 @@ thiserror.version = "^2"
|
||||||
proc-macro2.version = "^1"
|
proc-macro2.version = "^1"
|
||||||
quote.version = "^1"
|
quote.version = "^1"
|
||||||
syn.version = "^3"
|
syn.version = "^3"
|
||||||
|
## Testing
|
||||||
[package]
|
insta.version = "^1"
|
||||||
name = "sabry"
|
|
||||||
description = "Syntactically Awesome But RustY - crate that brings SCSS/SASS into rust"
|
|
||||||
keywords = ["web", "sass", "scss", "macro"]
|
|
||||||
repository.workspace = true
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
rust-version.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
## Workspace
|
|
||||||
sabry_procmacro = { workspace = true, optional = true }
|
|
||||||
sabry_build = { workspace = true, optional = true }
|
|
||||||
sabry_intrnl = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
leptos = { version = "^0.8" }
|
|
||||||
insta = { version = "^1", features = ["glob"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["procmacro"]
|
|
||||||
const-scoping = ["sabry_procmacro?/const-compile"]
|
|
||||||
procmacro = ["dep:sabry_procmacro"]
|
|
||||||
build = ["dep:sabry_build"]
|
|
||||||
net-files = ["sabry_build/net-files"]
|
|
||||||
internals = []
|
|
||||||
nightly = ["sabry_procmacro?/nightly"]
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
|
||||||
28
sabry/Cargo.toml
Normal file
28
sabry/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "sabry"
|
||||||
|
description = "Syntactically Awesome But RustY - crate that brings SCSS/SASS into rust"
|
||||||
|
keywords = ["web", "sass", "scss", "macro"]
|
||||||
|
repository.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
## Workspace
|
||||||
|
sabry_procmacro.workspace = true
|
||||||
|
sabry_intrnl.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
leptos.version = "^0.8"
|
||||||
|
insta.workspace = true
|
||||||
|
insta.features = ["glob"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
nightly = ["sabry_procmacro/nightly", "sabry_intrnl/nightly"]
|
||||||
4
sabry/src/lib.rs
Normal file
4
sabry/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#![doc = include_str!("../../README.md")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
pub use sabry_procmacro::*;
|
||||||
|
|
@ -14,10 +14,9 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sabry_intrnl.workspace = true
|
sabry_intrnl.workspace = true
|
||||||
sabry_intrnl.features = ["compile", "scope"]
|
sabry_intrnl.features = ["config-manifest", "style-compiler"]
|
||||||
|
sabry_procmacro.workspace = true
|
||||||
sabry_procmacro_impl.workspace = true
|
sabry_procmacro_impl.workspace = true
|
||||||
sabry_procmacro_impl.features = ["const-compile"]
|
|
||||||
|
|
||||||
proc-macro2.workspace = true
|
proc-macro2.workspace = true
|
||||||
proc-macro2.features = ["span-locations"]
|
proc-macro2.features = ["span-locations"]
|
||||||
syn.workspace = true
|
syn.workspace = true
|
||||||
|
|
@ -26,4 +25,4 @@ thiserror.workspace = true
|
||||||
walkdir.workspace = true
|
walkdir.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
net-files = []
|
nightly = ["sabry_procmacro/nightly", "sabry_procmacro_impl/nightly"]
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,17 @@ use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
fs::{self, OpenOptions},
|
fs::{self, OpenOptions},
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
path::PathBuf,
|
|
||||||
str::FromStr,
|
|
||||||
vec,
|
vec,
|
||||||
};
|
};
|
||||||
|
|
||||||
use sabry_intrnl::{
|
use sabry_intrnl::{
|
||||||
|
conf::{BehavHashCollision, BehavSassModCollision, SabryConfig, manifest::ManifestError},
|
||||||
|
style::{
|
||||||
compiler::{CompilerAdapter, SabryCompilerError},
|
compiler::{CompilerAdapter, SabryCompilerError},
|
||||||
config::{manifest::ManifestError, BehavHashCollision, BehavSassModCollision, SabryConfig},
|
scope::{ArbitraryScope, ScopeError, ScopeHash},
|
||||||
scoper::{hash::ScopeHash, ArbitraryScope, ScopeError},
|
},
|
||||||
};
|
};
|
||||||
use sabry_procmacro_impl::impls::{styly, ArbitraryStyleSyntax};
|
use sabry_procmacro_impl::impls::{ArbitraryStyleSyntax, styly};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::filevisit::{self, FileVisitError};
|
use crate::filevisit::{self, FileVisitError};
|
||||||
|
|
@ -71,7 +71,6 @@ pub struct SabryBuilder {
|
||||||
state: SabryBuildState,
|
state: SabryBuildState,
|
||||||
}
|
}
|
||||||
|
|
||||||
//🧙
|
|
||||||
impl SabryBuilder {
|
impl SabryBuilder {
|
||||||
/// Construct new [SabryBuilder] from the config
|
/// Construct new [SabryBuilder] from the config
|
||||||
///
|
///
|
||||||
|
|
@ -124,42 +123,40 @@ impl SabryBuilder {
|
||||||
pub fn generate_output(&mut self) -> BuilderResult {
|
pub fn generate_output(&mut self) -> BuilderResult {
|
||||||
// warn on empty loaded_css_modules
|
// warn on empty loaded_css_modules
|
||||||
if self.state.loaded_css_modules.is_empty() {
|
if self.state.loaded_css_modules.is_empty() {
|
||||||
println!("🧙 sabry didn't compile any CSS. Perhaps the crate has no styles? Lets write some!\nAlso you should check that you do use `styly!` macro properly - at the top level as an item.");
|
println!(
|
||||||
|
"🧙 sabry didn't compile any CSS. Perhaps the crate has no styles? Lets write some!\nAlso you should check that you do use `styly!` macro properly - at the top level as an item."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(scope_dir) = &self.config.css.scopes {
|
if let Some(scope_dir) = &self.config.css.scopes {
|
||||||
println!("🧙 writing CSS files for each of loaded scopes into {scope_dir}");
|
println!("🧙 writing CSS files for each of loaded scopes into {scope_dir:?}");
|
||||||
|
|
||||||
fs::create_dir_all(scope_dir)?;
|
fs::create_dir_all(scope_dir)?;
|
||||||
for (scope, code) in &self.state.loaded_css_modules {
|
for (scope, code) in &self.state.loaded_css_modules {
|
||||||
let scope_path = format!("{}/{}.css", scope_dir, scope);
|
let scope_path = scope_dir.join(scope).with_extension("css");
|
||||||
fs::write(scope_path, code)?;
|
fs::write(scope_path, code)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(bundle_file) = &self.config.css.bundle {
|
if let Some(bundle_file) = &self.config.css.bundle {
|
||||||
println!("🧙 writing merged CSS for the entire crate into {bundle_file}");
|
println!("🧙 writing merged CSS for the entire crate into {bundle_file:?}");
|
||||||
|
if let Some(dir) = bundle_file.parent() {
|
||||||
let path = PathBuf::from_str(bundle_file)?;
|
|
||||||
|
|
||||||
if let Some(dir) = path.parent() {
|
|
||||||
fs::create_dir_all(dir)?;
|
fs::create_dir_all(dir)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the file to write new fresh tasty CSS
|
// remove the file to write new fresh tasty CSS
|
||||||
let _ = fs::remove_file(&path);
|
let _ = fs::remove_file(bundle_file);
|
||||||
// and then reopen it
|
// and then reopen it
|
||||||
let mut file = OpenOptions::new()
|
let mut file = OpenOptions::new()
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(path)?;
|
.open(bundle_file)?;
|
||||||
|
|
||||||
// merged bundle CSS does require another lightningcss pass
|
// merged bundle CSS does require another lightningcss pass
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
for (_, code) in &self.state.loaded_css_modules {
|
for (_, code) in &self.state.loaded_css_modules {
|
||||||
buffer.push_str(code);
|
buffer.push_str(code);
|
||||||
}
|
}
|
||||||
buffer = self.css_compiler.lightningcss(&buffer)?;
|
|
||||||
|
|
||||||
write!(file, "{buffer}")?;
|
write!(file, "{buffer}")?;
|
||||||
}
|
}
|
||||||
|
|
@ -258,13 +255,12 @@ impl SabryBuilder {
|
||||||
let mut modules: Vec<StyleModule> = vec![];
|
let mut modules: Vec<StyleModule> = vec![];
|
||||||
if let Some(sass_mods) = &self.config.sass.modules {
|
if let Some(sass_mods) = &self.config.sass.modules {
|
||||||
for pre in sass_mods {
|
for pre in sass_mods {
|
||||||
let pre_path = PathBuf::from_str(pre)?;
|
let pre_name = pre
|
||||||
let pre_name = pre_path
|
|
||||||
.file_name()
|
.file_name()
|
||||||
.ok_or(SabryBuildError::FileName())?
|
.ok_or(SabryBuildError::FileName())?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
let code = fs::read_to_string(pre_path)?;
|
let code = fs::read_to_string(pre)?;
|
||||||
modules.push((pre_name, code));
|
modules.push((pre_name, code));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,9 +272,7 @@ impl SabryBuilder {
|
||||||
if let Some(sass_pres) = &self.config.sass.prelude {
|
if let Some(sass_pres) = &self.config.sass.prelude {
|
||||||
let mut sass_preludes: Vec<SassPreludeModule> = vec![];
|
let mut sass_preludes: Vec<SassPreludeModule> = vec![];
|
||||||
for pre in sass_pres {
|
for pre in sass_pres {
|
||||||
let pre_path = PathBuf::from_str(pre)?;
|
let syntax = pre
|
||||||
|
|
||||||
let syntax = pre_path
|
|
||||||
.extension()
|
.extension()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_str()
|
.to_str()
|
||||||
|
|
@ -287,24 +281,22 @@ impl SabryBuilder {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(SabryBuildError::Another(format!(
|
return Err(SabryBuildError::Another(format!(
|
||||||
"Unknown syntax for sass prelude {pre}"
|
"Unknown syntax for sass prelude {pre:?}"
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let code = fs::read_to_string(pre_path)?;
|
let code = fs::read_to_string(pre)?;
|
||||||
sass_preludes.push(SassPreludeModule { syntax, code });
|
sass_preludes.push(SassPreludeModule { syntax, code });
|
||||||
}
|
}
|
||||||
self.state.sass_prelude.extend(sass_preludes);
|
self.state.sass_prelude.extend(sass_preludes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// load css preludes
|
// load css preludes
|
||||||
if let Some(css_pre) = &self.config.css.prelude {
|
for pre in &self.config.css.prelude {
|
||||||
for pre in css_pre {
|
|
||||||
let code = fs::read_to_string(pre)?;
|
let code = fs::read_to_string(pre)?;
|
||||||
self.state.css_prelude.push_str(&code);
|
self.state.css_prelude.push_str(&code);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -319,9 +311,9 @@ impl SabryBuilder {
|
||||||
/// This function will not overwrite the module file. Never.
|
/// This function will not overwrite the module file. Never.
|
||||||
///
|
///
|
||||||
pub fn load_side_module(&mut self, name: ModuleName, code: ModuleCode) -> BuilderResult {
|
pub fn load_side_module(&mut self, name: ModuleName, code: ModuleCode) -> BuilderResult {
|
||||||
println!("🧙 loading side module '{}'", &name);
|
println!("🧙 loading side module '{}'", name);
|
||||||
|
|
||||||
let module_file_path = format!("{}/{}", &self.config.sass.intermediate_dir, &name);
|
let module_file_path = self.config.sass.intermediate_dir.join(&name);
|
||||||
let mut open_options = OpenOptions::new();
|
let mut open_options = OpenOptions::new();
|
||||||
let mut open_options = open_options.write(true);
|
let mut open_options = open_options.write(true);
|
||||||
|
|
||||||
|
|
@ -333,10 +325,12 @@ impl SabryBuilder {
|
||||||
open_options = if self.state.known_side_modules.contains(&name) {
|
open_options = if self.state.known_side_modules.contains(&name) {
|
||||||
match &self.config.sass.module_name_collision {
|
match &self.config.sass.module_name_collision {
|
||||||
BehavSassModCollision::Error => {
|
BehavSassModCollision::Error => {
|
||||||
return Err(SabryBuildError::ModuleCollision { module: name })
|
return Err(SabryBuildError::ModuleCollision { module: name });
|
||||||
}
|
}
|
||||||
BehavSassModCollision::Merge => {
|
BehavSassModCollision::Merge => {
|
||||||
println!("🧙 sabry found duplicate module name '{name}': merging code, as configured");
|
println!(
|
||||||
|
"🧙 sabry found duplicate module name '{name}': merging code, as configured"
|
||||||
|
);
|
||||||
open_options.append(true)
|
open_options.append(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,7 +393,9 @@ pub enum SabryBuildError {
|
||||||
FileVisit(#[from] FileVisitError),
|
FileVisit(#[from] FileVisitError),
|
||||||
#[error("Syntax of style can not be parsed")]
|
#[error("Syntax of style can not be parsed")]
|
||||||
SyntaxError(#[from] ScopeError),
|
SyntaxError(#[from] ScopeError),
|
||||||
#[error("Another scope has the same hash, and sabry is configured to raise an error. Try to adjust config, increase hash size, or change the style code")]
|
#[error(
|
||||||
|
"Another scope has the same hash, and sabry is configured to raise an error. Try to adjust config, increase hash size, or change the style code"
|
||||||
|
)]
|
||||||
HashCollision { scope: ModuleName },
|
HashCollision { scope: ModuleName },
|
||||||
#[error("Something's wrong with file path")]
|
#[error("Something's wrong with file path")]
|
||||||
Path(#[from] Infallible),
|
Path(#[from] Infallible),
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,6 @@
|
||||||
|
|
||||||
pub mod buildmagic;
|
pub mod buildmagic;
|
||||||
pub mod filevisit;
|
pub mod filevisit;
|
||||||
|
|
||||||
|
pub use buildmagic::buildy;
|
||||||
|
pub use sabry_procmacro::usey;
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,21 @@ all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
## Css
|
||||||
|
raffia.version = "^0.13"
|
||||||
|
sasso.workspace = true
|
||||||
|
sasso.optional = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
base64.optional = true
|
## Codegen
|
||||||
grass.workspace = true
|
|
||||||
grass.optional = true
|
|
||||||
lightningcss.workspace = true
|
|
||||||
lightningcss.optional = true
|
|
||||||
raffia.workspace = true
|
|
||||||
syn.workspace = true
|
|
||||||
|
|
||||||
cfg-if.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde.features = ["derive"]
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
syn.workspace = true
|
||||||
|
## Manifets seri
|
||||||
|
serde.workspace = true
|
||||||
|
serde.optional = true
|
||||||
|
serde.features = ["derive"]
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
toml.optional = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
compile = ["dep:grass", "dep:lightningcss"]
|
config-manifest = ["dep:toml", "dep:serde"]
|
||||||
scope = ["dep:base64"]
|
style-compiler = ["dep:sasso"]
|
||||||
|
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
use crate::config::SabryConfig;
|
|
||||||
|
|
||||||
/// Convenience wrapper on Grass and Lightningcss
|
|
||||||
pub struct CompilerAdapter {
|
|
||||||
#[allow(dead_code)] // without 'compile' feature, still want to set this
|
|
||||||
config: SabryConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompilerAdapter {
|
|
||||||
pub fn new(config: SabryConfig) -> Self {
|
|
||||||
Self { config }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compile given SASS/SCSS into CSS
|
|
||||||
/// with respect to self.config
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
pub fn compile_module(
|
|
||||||
&self,
|
|
||||||
syntax: crate::syntax::ostrta::OneSyntaxToRuleThemAll,
|
|
||||||
code: &str,
|
|
||||||
) -> Result<String, SabryCompilerError> {
|
|
||||||
let options = grass::Options::from(&self.config).input_syntax(syntax.into());
|
|
||||||
|
|
||||||
let css = self.lightningcss(&grass::from_string(code, &options)?)?;
|
|
||||||
|
|
||||||
Ok(css)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform lightningcss transformations on given css
|
|
||||||
/// with respect to self.config.css.minify on minification
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
pub fn lightningcss(&self, css: &str) -> Result<String, SabryCompilerError> {
|
|
||||||
use lightningcss::{printer::PrinterOptions, stylesheet::MinifyOptions, targets::Targets};
|
|
||||||
|
|
||||||
let mut lightsheet = match lightningcss::stylesheet::StyleSheet::parse(
|
|
||||||
css,
|
|
||||||
lightningcss::stylesheet::ParserOptions::default(),
|
|
||||||
) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(SabryCompilerError::LightParse {
|
|
||||||
kind: e.kind.to_string(),
|
|
||||||
loc: e.loc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let targets = Targets {
|
|
||||||
browsers: Some(self.config.lightningcss.targets.clone().into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let printer_options = PrinterOptions {
|
|
||||||
minify: self.config.css.minify,
|
|
||||||
targets,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let minify_options = MinifyOptions {
|
|
||||||
targets,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.config.css.minify {
|
|
||||||
lightsheet.minify(minify_options)?
|
|
||||||
}
|
|
||||||
|
|
||||||
let css = match lightsheet.to_css(printer_options) {
|
|
||||||
Ok(c) => {
|
|
||||||
drop(lightsheet);
|
|
||||||
c.code
|
|
||||||
}
|
|
||||||
Err(e) => return Err(SabryCompilerError::LightPrint(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(css)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
impl From<&SabryConfig> for grass::Options<'_> {
|
|
||||||
fn from(config: &SabryConfig) -> Self {
|
|
||||||
let mut options = grass::Options::default();
|
|
||||||
options = options.style(if config.css.minify {
|
|
||||||
grass::OutputStyle::Compressed
|
|
||||||
} else {
|
|
||||||
grass::OutputStyle::Expanded
|
|
||||||
});
|
|
||||||
options = options.load_path(&config.sass.intermediate_dir);
|
|
||||||
|
|
||||||
options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
#[derive(thiserror::Error)]
|
|
||||||
pub enum SabryCompilerError {
|
|
||||||
#[error("Could not compile into CSS with grass")]
|
|
||||||
GrassCompile(#[from] Box<grass::Error>),
|
|
||||||
#[error("Could not print compiled CSS with lightningcss")]
|
|
||||||
LightPrint(#[from] lightningcss::error::Error<lightningcss::error::PrinterErrorKind>),
|
|
||||||
#[error("Could not minify CSS with lightningcss")]
|
|
||||||
LightMinify(#[from] lightningcss::error::Error<lightningcss::error::MinifyErrorKind>),
|
|
||||||
#[error("Could not parse CSS with lightningcss")]
|
|
||||||
LightParse {
|
|
||||||
kind: String,
|
|
||||||
loc: Option<lightningcss::error::ErrorLocation>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
impl std::fmt::Debug for SabryCompilerError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let explain = match self {
|
|
||||||
Self::LightPrint(e) => format!(
|
|
||||||
"{} at {:?}",
|
|
||||||
e.kind,
|
|
||||||
e.loc
|
|
||||||
.clone()
|
|
||||||
.map(|l| format!("file {}, line {}, col {}", l.filename, l.line, l.column))
|
|
||||||
),
|
|
||||||
Self::GrassCompile(err) => format!("{err}"),
|
|
||||||
Self::LightMinify(e) => format!(
|
|
||||||
"{} at {:?}",
|
|
||||||
e.kind,
|
|
||||||
e.loc
|
|
||||||
.clone()
|
|
||||||
.map(|l| format!("file {}, line {}, col {}", l.filename, l.line, l.column))
|
|
||||||
),
|
|
||||||
Self::LightParse { kind, loc } => format!(
|
|
||||||
"{kind} at {:?}",
|
|
||||||
loc.clone()
|
|
||||||
.map(|l| format!("file {}, line {}, col {}", l.filename, l.line, l.column))
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(f, "{explain}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
sabry_intrnl/src/conf/manifest.rs
Normal file
44
sabry_intrnl/src/conf/manifest.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
use std::{env, fs, io};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::SabryConfig;
|
||||||
|
|
||||||
|
pub const MANIFEST: &str = "Cargo.toml";
|
||||||
|
pub const MANIFEST_DIR_ENV: &str = "CARGO_MANIFEST_DIR";
|
||||||
|
|
||||||
|
pub fn read_manifest() -> Result<ValuableManifest, ManifestError> {
|
||||||
|
let manpath = format!("{}/{MANIFEST}", env::var(MANIFEST_DIR_ENV)?);
|
||||||
|
|
||||||
|
Ok(toml::de::from_str(&fs::read_to_string(manpath)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ManifestError {
|
||||||
|
#[error("Could not read manifest path from env")]
|
||||||
|
ManifestDirEnv(#[from] env::VarError),
|
||||||
|
#[error("Could not read manifest file")]
|
||||||
|
ManifestRead(#[from] io::Error),
|
||||||
|
#[error("Could not deserialize manifest")]
|
||||||
|
ManifestDe(#[from] toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manifest structure that makes sense for sabry
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ValuableManifest {
|
||||||
|
pub package: Option<ValuableManifestPkg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manifest \[package.metadata\] structure that makes sense for sabry
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ValuableManifestPkg {
|
||||||
|
pub metadata: Option<ValuableManifestPkgMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manifest \[package.metadata.sabry\] structure that makes sense for sabry
|
||||||
|
///
|
||||||
|
/// Which is basically [SabryConfig]
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ValuableManifestPkgMeta {
|
||||||
|
pub sabry: Option<SabryConfig>,
|
||||||
|
}
|
||||||
210
sabry_intrnl/src/conf/mod.rs
Normal file
210
sabry_intrnl/src/conf/mod.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
use std::{path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
#[cfg(feature = "config-manifest")]
|
||||||
|
pub mod manifest;
|
||||||
|
|
||||||
|
/// Sabry configuration, as it is from package.metadata.sabry
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
#[cfg_attr(feature = "config-manifest", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "config-manifest", serde(default))]
|
||||||
|
pub struct SabryConfig {
|
||||||
|
pub css: SabryCssConfig,
|
||||||
|
pub sass: SabrySassConfig,
|
||||||
|
pub hash: SabryHashConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SabryConfig {
|
||||||
|
/// Require sabry configuration for the current project from every place in the code
|
||||||
|
#[cfg(feature = "config-manifest")]
|
||||||
|
pub fn require() -> Result<Self, manifest::ManifestError> {
|
||||||
|
let manifest = manifest::read_manifest()?;
|
||||||
|
Ok(Self::from(&manifest))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "config-manifest")]
|
||||||
|
impl From<&manifest::ValuableManifest> for SabryConfig {
|
||||||
|
fn from(value: &manifest::ValuableManifest) -> Self {
|
||||||
|
value
|
||||||
|
.package
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.metadata.as_ref())
|
||||||
|
.and_then(|m| m.sabry.as_ref())
|
||||||
|
.map_or(Self::default(), |sbr| sbr.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sabry `css` configuration table
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(feature = "config-manifest", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "config-manifest", serde(default))]
|
||||||
|
pub struct SabryCssConfig {
|
||||||
|
pub bundle: Option<PathBuf>,
|
||||||
|
pub scopes: Option<PathBuf>,
|
||||||
|
pub prelude: Vec<PathBuf>,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryCssConfig::default_minify")
|
||||||
|
)]
|
||||||
|
pub minify: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SabryCssConfig {
|
||||||
|
fn default_minify() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SabryCssConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bundle: None,
|
||||||
|
scopes: None,
|
||||||
|
prelude: vec![],
|
||||||
|
minify: Self::default_minify(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sabry `sass` configuration table
|
||||||
|
///
|
||||||
|
/// (SASS/SCSS related config)
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(feature = "config-manifest", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "config-manifest", serde(default))]
|
||||||
|
pub struct SabrySassConfig {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabrySassConfig::default_scanroot")
|
||||||
|
)]
|
||||||
|
pub scanroot: PathBuf,
|
||||||
|
pub modules: Option<Vec<PathBuf>>,
|
||||||
|
pub prelude: Option<Vec<PathBuf>>,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabrySassConfig::default_intermediate_dir")
|
||||||
|
)]
|
||||||
|
pub intermediate_dir: PathBuf,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabrySassConfig::default_module_name_collision")
|
||||||
|
)]
|
||||||
|
pub module_name_collision: BehavSassModCollision,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SabrySassConfig {
|
||||||
|
fn default_scanroot() -> PathBuf {
|
||||||
|
PathBuf::from_str("src")
|
||||||
|
.expect("Infallible: default SabrySassConfig::scanroot into PathBuf")
|
||||||
|
}
|
||||||
|
fn default_intermediate_dir() -> PathBuf {
|
||||||
|
PathBuf::from_str("target/.sabry/sass")
|
||||||
|
.expect("Infallibe: default SabrySassConfig::intermediate_dir into PathBuf")
|
||||||
|
}
|
||||||
|
fn default_module_name_collision() -> BehavSassModCollision {
|
||||||
|
BehavSassModCollision::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SabrySassConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
scanroot: Self::default_scanroot(),
|
||||||
|
modules: None,
|
||||||
|
prelude: None,
|
||||||
|
intermediate_dir: Self::default_intermediate_dir(),
|
||||||
|
module_name_collision: Self::default_module_name_collision(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sabry `hash` configuration table
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(feature = "config-manifest", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "config-manifest", serde(default))]
|
||||||
|
pub struct SabryHashConfig {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryHashConfig::default_size")
|
||||||
|
)]
|
||||||
|
pub size: usize,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryHashConfig::default_collision")
|
||||||
|
)]
|
||||||
|
pub collision: BehavHashCollision,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryHashConfig::default_use_scope_name")
|
||||||
|
)]
|
||||||
|
pub use_scope_name: bool,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryHashConfig::default_use_item_names")
|
||||||
|
)]
|
||||||
|
pub use_item_names: bool,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryHashConfig::default_use_code_size")
|
||||||
|
)]
|
||||||
|
pub use_code_size: bool,
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "config-manifest",
|
||||||
|
serde(default = "SabryHashConfig::default_use_code_text")
|
||||||
|
)]
|
||||||
|
pub use_code_text: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SabryHashConfig {
|
||||||
|
fn default_size() -> usize {
|
||||||
|
6
|
||||||
|
}
|
||||||
|
fn default_collision() -> BehavHashCollision {
|
||||||
|
BehavHashCollision::default()
|
||||||
|
}
|
||||||
|
fn default_use_scope_name() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn default_use_item_names() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn default_use_code_size() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn default_use_code_text() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SabryHashConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
size: Self::default_size(),
|
||||||
|
collision: Self::default_collision(),
|
||||||
|
use_scope_name: Self::default_use_scope_name(),
|
||||||
|
use_code_size: Self::default_use_code_size(),
|
||||||
|
use_code_text: Self::default_use_code_text(),
|
||||||
|
use_item_names: Self::default_use_item_names(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \[package.metadata.sabry.sass\].module_name_collision option
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "config-manifest", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "config-manifest", serde(rename_all = "lowercase"))]
|
||||||
|
pub enum BehavSassModCollision {
|
||||||
|
#[default]
|
||||||
|
Merge,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \[package.metadata.sabry.hash\].collision option
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "config-manifest", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "config-manifest", serde(rename_all = "lowercase"))]
|
||||||
|
pub enum BehavHashCollision {
|
||||||
|
#[default]
|
||||||
|
Ignore,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
pub mod manifest;
|
|
||||||
use manifest::{ManifestError, ValuableManifest};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
/// Sabry configuration, as it is from package.metadata.sabry
|
|
||||||
#[derive(Default, Deserialize, Clone)]
|
|
||||||
pub struct SabryConfig {
|
|
||||||
#[serde(default = "SabryCssConfig::default")]
|
|
||||||
pub css: SabryCssConfig,
|
|
||||||
#[serde(default = "SabrySassConfig::default")]
|
|
||||||
pub sass: SabrySassConfig,
|
|
||||||
#[serde(default = "SabryHashConfig::default")]
|
|
||||||
pub hash: SabryHashConfig,
|
|
||||||
#[serde(default = "SabryGrassConfig::default")]
|
|
||||||
pub grass: SabryGrassConfig,
|
|
||||||
#[serde(default = "SabryLightCssConfig::default")]
|
|
||||||
pub lightningcss: SabryLightCssConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SabryConfig {
|
|
||||||
/// Require sabry configuration for the current project from every place in the code
|
|
||||||
pub fn require() -> Result<Self, ManifestError> {
|
|
||||||
let manifest = manifest::read_manifest()?;
|
|
||||||
Ok(Self::from(&manifest))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&ValuableManifest> for SabryConfig {
|
|
||||||
fn from(value: &ValuableManifest) -> Self {
|
|
||||||
value
|
|
||||||
.package
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| p.metadata.as_ref())
|
|
||||||
.and_then(|m| m.sabry.as_ref())
|
|
||||||
.map_or(Self::default(), |sbr| sbr.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sabry `css` configuration table
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct SabryCssConfig {
|
|
||||||
pub bundle: Option<String>,
|
|
||||||
pub scopes: Option<String>,
|
|
||||||
pub prelude: Option<Vec<String>>,
|
|
||||||
#[serde(default = "SabryCssConfig::default_minify")]
|
|
||||||
pub minify: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SabryCssConfig {
|
|
||||||
fn default_minify() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SabryCssConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
bundle: None,
|
|
||||||
scopes: None,
|
|
||||||
prelude: None,
|
|
||||||
minify: Self::default_minify(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sabry `sass` configuration table
|
|
||||||
///
|
|
||||||
/// (SASS/SCSS related config)
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct SabrySassConfig {
|
|
||||||
#[serde(default = "SabrySassConfig::default_scanroot")]
|
|
||||||
pub scanroot: String,
|
|
||||||
pub modules: Option<Vec<String>>,
|
|
||||||
pub prelude: Option<Vec<String>>,
|
|
||||||
#[serde(default = "SabrySassConfig::default_intermediate_dir")]
|
|
||||||
pub intermediate_dir: String,
|
|
||||||
#[serde(default = "SabrySassConfig::default_module_name_collision")]
|
|
||||||
pub module_name_collision: BehavSassModCollision,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SabrySassConfig {
|
|
||||||
fn default_scanroot() -> String {
|
|
||||||
"src".into()
|
|
||||||
}
|
|
||||||
fn default_intermediate_dir() -> String {
|
|
||||||
"target/.sabry/sass".into()
|
|
||||||
}
|
|
||||||
fn default_module_name_collision() -> BehavSassModCollision {
|
|
||||||
BehavSassModCollision::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SabrySassConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
scanroot: Self::default_scanroot(),
|
|
||||||
modules: None,
|
|
||||||
prelude: None,
|
|
||||||
intermediate_dir: Self::default_intermediate_dir(),
|
|
||||||
module_name_collision: Self::default_module_name_collision(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sabry `hash` configuration table
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct SabryHashConfig {
|
|
||||||
#[serde(default = "SabryHashConfig::default_size")]
|
|
||||||
pub size: usize,
|
|
||||||
#[serde(default = "SabryHashConfig::default_collision")]
|
|
||||||
pub collision: BehavHashCollision,
|
|
||||||
#[serde(default = "SabryHashConfig::default_use_scope_name")]
|
|
||||||
pub use_scope_name: bool,
|
|
||||||
#[serde(default = "SabryHashConfig::default_use_item_names")]
|
|
||||||
pub use_item_names: bool,
|
|
||||||
#[serde(default = "SabryHashConfig::default_use_code_size")]
|
|
||||||
pub use_code_size: bool,
|
|
||||||
#[serde(default = "SabryHashConfig::default_use_code_text")]
|
|
||||||
pub use_code_text: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SabryHashConfig {
|
|
||||||
fn default_size() -> usize {
|
|
||||||
6
|
|
||||||
}
|
|
||||||
fn default_collision() -> BehavHashCollision {
|
|
||||||
BehavHashCollision::default()
|
|
||||||
}
|
|
||||||
fn default_use_scope_name() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fn default_use_item_names() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_use_code_size() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fn default_use_code_text() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SabryHashConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
size: Self::default_size(),
|
|
||||||
collision: Self::default_collision(),
|
|
||||||
use_scope_name: Self::default_use_scope_name(),
|
|
||||||
use_code_size: Self::default_use_code_size(),
|
|
||||||
use_code_text: Self::default_use_code_text(),
|
|
||||||
use_item_names: Self::default_use_item_names(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [grass] specific configuration for sabry
|
|
||||||
#[derive(Default, Deserialize, Clone)]
|
|
||||||
pub struct SabryGrassConfig {}
|
|
||||||
|
|
||||||
/// [lightningcss] specific configuration for sabry
|
|
||||||
#[derive(Default, Deserialize, Clone)]
|
|
||||||
pub struct SabryLightCssConfig {
|
|
||||||
pub targets: SabryLightTargets,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// \[package.metadata.sabry.lightningcss.targets\] configuration.
|
|
||||||
///
|
|
||||||
/// This is a convenience layer under [lightningcss::targets::Browsers] structure accepted
|
|
||||||
/// by lightningcss.
|
|
||||||
///
|
|
||||||
/// This structure is a part of [SabryConfig], which faces developer with more convenient
|
|
||||||
/// `safari = "13.2"` syntax.
|
|
||||||
#[derive(Default, Deserialize, Clone)]
|
|
||||||
pub struct SabryLightTargets {
|
|
||||||
pub android: Option<String>,
|
|
||||||
pub chrome: Option<String>,
|
|
||||||
pub edge: Option<String>,
|
|
||||||
pub firefox: Option<String>,
|
|
||||||
pub ie: Option<String>,
|
|
||||||
pub ios_saf: Option<String>,
|
|
||||||
pub opera: Option<String>,
|
|
||||||
pub safari: Option<String>,
|
|
||||||
pub samsung: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SabryLightTargets {
|
|
||||||
pub fn parse_ver(ver: &str) -> u32 {
|
|
||||||
let mut vercode: u32 = 0;
|
|
||||||
let vers = ver.split('.');
|
|
||||||
for (i, dig) in vers.into_iter().enumerate() {
|
|
||||||
let dignum = dig
|
|
||||||
.parse::<u32>()
|
|
||||||
.unwrap_or_else(|_| panic!("Invalid browser specifier: '{dig}'"));
|
|
||||||
|
|
||||||
let shift = 16 - 8 * i;
|
|
||||||
vercode |= dignum << shift;
|
|
||||||
}
|
|
||||||
vercode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
impl From<SabryLightTargets> for lightningcss::targets::Browsers {
|
|
||||||
fn from(value: SabryLightTargets) -> Self {
|
|
||||||
Self {
|
|
||||||
android: value.android.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
chrome: value.chrome.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
edge: value.edge.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
firefox: value.firefox.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
ie: value.ie.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
ios_saf: value.ios_saf.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
opera: value.opera.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
safari: value.safari.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
samsung: value.samsung.map(|s| SabryLightTargets::parse_ver(&s)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// \[package.metadata.sabry.sass\].module_name_collision option
|
|
||||||
#[derive(Default, Deserialize, Clone, Copy)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum BehavSassModCollision {
|
|
||||||
#[default]
|
|
||||||
Merge,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// \[package.metadata.sabry.hash\].collision option
|
|
||||||
#[derive(Default, Deserialize, Clone, Copy)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum BehavHashCollision {
|
|
||||||
#[default]
|
|
||||||
Ignore,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::{manifest, SabryConfig};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_manifest_still_worth_a_read() {
|
|
||||||
let manifest = manifest::read_manifest().unwrap();
|
|
||||||
let _conf = SabryConfig::from(&manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,2 @@
|
||||||
#![doc = include_str!("../README.md")]
|
pub mod conf;
|
||||||
|
pub mod style;
|
||||||
pub mod compiler;
|
|
||||||
pub mod config;
|
|
||||||
pub mod scoper;
|
|
||||||
pub mod syntax;
|
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
/// Convenience wrapper for String-being-a-hash
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ScopeHash(String);
|
|
||||||
|
|
||||||
impl ScopeHash {
|
|
||||||
#[cfg(feature = "scope")]
|
|
||||||
pub fn new(scope: &super::ArbitraryScope, config: &crate::config::SabryHashConfig) -> Self {
|
|
||||||
use base64::Engine;
|
|
||||||
use raffia::ast::InterpolableIdent;
|
|
||||||
use std::hash::Hasher;
|
|
||||||
|
|
||||||
use crate::scoper::sanitize_css_member;
|
|
||||||
|
|
||||||
let mut hasher = std::hash::DefaultHasher::new();
|
|
||||||
|
|
||||||
if config.use_scope_name {
|
|
||||||
hasher.write(scope.name.to_string().as_bytes());
|
|
||||||
}
|
|
||||||
if config.use_code_text {
|
|
||||||
hasher.write(scope.adapter().source().as_bytes());
|
|
||||||
}
|
|
||||||
if config.use_code_size {
|
|
||||||
hasher.write(&scope.adapter().source().len().to_ne_bytes());
|
|
||||||
}
|
|
||||||
if config.use_item_names {
|
|
||||||
let classes = scope.adapter().class_selectors();
|
|
||||||
let ids = scope.adapter().id_selectors();
|
|
||||||
//# types/tags dont participate
|
|
||||||
//# child-parent dont participate
|
|
||||||
|
|
||||||
let merged_items = classes
|
|
||||||
.iter()
|
|
||||||
.map(|s| &s.name)
|
|
||||||
.chain(ids.iter().map(|i| &i.name))
|
|
||||||
.map(|ident| match ident {
|
|
||||||
InterpolableIdent::Literal(lit) => Some(lit),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.filter_map(|mbl| mbl.map(|l| l.raw))
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
hasher.write(merged_items.as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
let hash = hasher.finish().to_be_bytes();
|
|
||||||
|
|
||||||
let hash = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(hash);
|
|
||||||
let hash = sanitize_css_member(&hash);
|
|
||||||
|
|
||||||
Self(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn test_init(v: String) -> Self {
|
|
||||||
Self(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use syn::Ident;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::SabryHashConfig, scoper::ArbitraryScope, syntax::ostrta::OneSyntaxToRuleThemAll,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::ScopeHash;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hash_matches() {
|
|
||||||
let source = ".cls1{color:red; &-dark{color: black} #id1 {color:green;} div {color:blue;}} .cls3#id2{color: black;}";
|
|
||||||
|
|
||||||
let scope1 = ArbitraryScope::from_source(
|
|
||||||
OneSyntaxToRuleThemAll::Scss,
|
|
||||||
syn::parse_str::<Ident>("lasifudm").unwrap(),
|
|
||||||
source,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let hash1 = ScopeHash::new(&scope1, &SabryHashConfig::default());
|
|
||||||
|
|
||||||
let scope2 = ArbitraryScope::from_source(
|
|
||||||
OneSyntaxToRuleThemAll::Scss,
|
|
||||||
syn::parse_str::<Ident>("lasifudm").unwrap(),
|
|
||||||
source,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let hash2 = ScopeHash::new(&scope2, &SabryHashConfig::default());
|
|
||||||
|
|
||||||
assert_eq!(hash1, hash2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
sabry_intrnl/src/style/compiler.rs
Normal file
49
sabry_intrnl/src/style/compiler.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use crate::{conf::SabryConfig, style::syntax::SyntaxKind};
|
||||||
|
|
||||||
|
/// Convenience wrapper on Grass and Lightningcss
|
||||||
|
pub struct CompilerAdapter {
|
||||||
|
#[allow(dead_code)] // without 'compile' feature, still want to set this
|
||||||
|
config: SabryConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompilerAdapter {
|
||||||
|
pub fn new(config: SabryConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile given SASS/SCSS into CSS
|
||||||
|
/// with respect to self.config
|
||||||
|
pub fn compile_module(
|
||||||
|
&self,
|
||||||
|
syntax: SyntaxKind,
|
||||||
|
code: &str,
|
||||||
|
) -> Result<String, SabryCompilerError> {
|
||||||
|
let importer = sasso::FsImporter::new(self.config.sass.modules.clone().unwrap_or_default());
|
||||||
|
let options = sasso::Options::new()
|
||||||
|
.with_style(if self.config.css.minify {
|
||||||
|
sasso::OutputStyle::Compressed
|
||||||
|
} else {
|
||||||
|
sasso::OutputStyle::Expanded
|
||||||
|
})
|
||||||
|
.with_importer(&importer)
|
||||||
|
.with_syntax(syntax.into());
|
||||||
|
|
||||||
|
Ok(sasso::compile(code, &options)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error)]
|
||||||
|
pub enum SabryCompilerError {
|
||||||
|
#[error("Could not compile into CSS with sasso")]
|
||||||
|
Compile(#[from] sasso::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SabryCompilerError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let explain = match self {
|
||||||
|
Self::Compile(err) => format!("{err}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{explain}")
|
||||||
|
}
|
||||||
|
}
|
||||||
5
sabry_intrnl/src/style/mod.rs
Normal file
5
sabry_intrnl/src/style/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod scope;
|
||||||
|
pub mod syntax;
|
||||||
|
|
||||||
|
#[cfg(feature = "style-compiler")]
|
||||||
|
pub mod compiler;
|
||||||
588
sabry_intrnl/src/style/scope.rs
Normal file
588
sabry_intrnl/src/style/scope.rs
Normal file
|
|
@ -0,0 +1,588 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use raffia::{
|
||||||
|
Span, Spanned,
|
||||||
|
ast::{InterpolableIdent, TypeSelector},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
conf::SabryHashConfig,
|
||||||
|
style::syntax::{StylesheetAdapter, SyntaxKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error)]
|
||||||
|
pub enum ScopeError {
|
||||||
|
#[error("Raffia reports parse error")]
|
||||||
|
Raffia(raffia::error::ErrorKind, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for ScopeError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let a = match self {
|
||||||
|
Self::Raffia(kind, source) => format!("{kind}: at {source}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{a}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct which represents arbitrary style scope with known style syntax and name identifier
|
||||||
|
pub struct ArbitraryScope<'s> {
|
||||||
|
/// Ident name for the current scope
|
||||||
|
pub name: syn::Ident,
|
||||||
|
adapter: StylesheetAdapter<'s>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> ArbitraryScope<'s> {
|
||||||
|
/// Parse given source code with given syntax and assign given name
|
||||||
|
pub fn from_source(
|
||||||
|
syntax: SyntaxKind,
|
||||||
|
name: syn::Ident,
|
||||||
|
source: &'s str,
|
||||||
|
) -> Result<Self, ScopeError> {
|
||||||
|
let adapter = match StylesheetAdapter::new(source, syntax) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => {
|
||||||
|
let source = &source[e.span.start..e.span.end];
|
||||||
|
return Err(ScopeError::Raffia(e.kind, source.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { adapter, name })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume arbitrary scope and create a [HashedScope] in its basis
|
||||||
|
///
|
||||||
|
/// This function calls [HashedScope::new] under the hood
|
||||||
|
pub fn hashed(self, config: &SabryHashConfig) -> Result<HashedScope<'s>, ScopeError> {
|
||||||
|
let hash = ScopeHash::new(&self, config);
|
||||||
|
HashedScope::new(hash, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow the underlying [StylesheetAdapter]
|
||||||
|
pub fn adapter(&self) -> &StylesheetAdapter<'s> {
|
||||||
|
&self.adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hashed scope which holds hashed style code,
|
||||||
|
/// vector of hashed selectors and the calculated hash itself.
|
||||||
|
///
|
||||||
|
/// Construction with [HashedScope::new] consumes [ArbitraryScope] and does all the job
|
||||||
|
pub struct HashedScope<'s> {
|
||||||
|
/// Original arbitrary scope
|
||||||
|
pub original_scope: ArbitraryScope<'s>,
|
||||||
|
/// Hashed style code
|
||||||
|
pub hashed_code: String,
|
||||||
|
/// Collection of hashed selectors
|
||||||
|
pub hashed_selectors: Vec<HashedSelector>,
|
||||||
|
|
||||||
|
/// Given hash to the scope
|
||||||
|
pub hash: ScopeHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> HashedScope<'s> {
|
||||||
|
/// Consume [ArbitraryScope],
|
||||||
|
/// calculate the hash,
|
||||||
|
/// hash all the supported selectors
|
||||||
|
/// and construct hashed source code
|
||||||
|
///
|
||||||
|
/// Heavy operation.
|
||||||
|
pub fn new(hash: ScopeHash, scope: ArbitraryScope<'s>) -> Result<Self, ScopeError> {
|
||||||
|
let origin_code = scope.adapter().source();
|
||||||
|
let mut hashed_code = String::with_capacity(scope.adapter().source().len());
|
||||||
|
|
||||||
|
// get classes
|
||||||
|
let classes = scope.adapter().class_selectors();
|
||||||
|
let classes = classes.iter().map(|c| HashedSelector::from_class(&hash, c));
|
||||||
|
|
||||||
|
// get ids
|
||||||
|
let ids = scope.adapter().id_selectors();
|
||||||
|
let ids = ids.iter().map(|c| HashedSelector::from_id(&hash, c));
|
||||||
|
|
||||||
|
// get tags
|
||||||
|
let tags = scope.adapter().type_selectors();
|
||||||
|
let tags = tags
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| match t {
|
||||||
|
TypeSelector::TagName(tag) => Some(tag),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|c| HashedSelector::from_tag(&hash, c));
|
||||||
|
|
||||||
|
// get global selectors
|
||||||
|
let globs = scope.adapter().glob_modified_selectors();
|
||||||
|
let globs = globs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|g| {
|
||||||
|
// we do ignore the latter selectors from :global(a, b) because there's no clear decision:
|
||||||
|
// how should we treat :glob(a, b) c {}
|
||||||
|
// as `a c {}` `b c {}`
|
||||||
|
// or as `a, b c {}` ?
|
||||||
|
g.arg.clone().and_then(|a| {
|
||||||
|
if let raffia::ast::PseudoClassSelectorArgKind::SelectorList(list) = a.kind {
|
||||||
|
list.selectors
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.map(|fcl| (g.span.clone(), fcl.span.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|c| HashedSelector::from_glob_mod(&hash, c, scope.adapter().source()));
|
||||||
|
|
||||||
|
let mut hashed_selectors = classes
|
||||||
|
.chain(ids)
|
||||||
|
.chain(tags)
|
||||||
|
.chain(globs)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// sorting by span start is important because of how hashed code construction works
|
||||||
|
hashed_selectors.sort_by(|a, b| {
|
||||||
|
let astart = a.sel.as_arbitrary().span.start;
|
||||||
|
let bstart = b.sel.as_arbitrary().span.start;
|
||||||
|
|
||||||
|
astart.cmp(&bstart)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut last_term_span: usize = 0;
|
||||||
|
for sel in &hashed_selectors {
|
||||||
|
let span = sel.sel.as_arbitrary().span.clone();
|
||||||
|
hashed_code.push_str(&origin_code[last_term_span..span.start]);
|
||||||
|
hashed_code.push_str(&sel.css_ident);
|
||||||
|
last_term_span = span.end;
|
||||||
|
}
|
||||||
|
hashed_code.push_str(&origin_code[last_term_span..]);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
original_scope: scope,
|
||||||
|
hashed_code,
|
||||||
|
hashed_selectors,
|
||||||
|
hash,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct which represents hashed selector inside of [HashedScope].
|
||||||
|
///
|
||||||
|
/// Does not hold information about hash itself, as meant to be inside of scope.
|
||||||
|
#[derive(PartialEq, Eq, Hash)]
|
||||||
|
pub struct HashedSelector {
|
||||||
|
/// Basic unrelated to hash info about this selector
|
||||||
|
pub sel: ScopedSelector,
|
||||||
|
/// CSS-ish identifier for this selector, which matches what's in the hashed style code
|
||||||
|
pub css_ident: String,
|
||||||
|
/// HTML-ish identifier for this selector, which will match [Self::css_ident] in browser
|
||||||
|
pub html_ident: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HashedSelector {
|
||||||
|
pub fn from_glob_mod(hash: &ScopeHash, sel: (Span, Span), source: &str) -> Self {
|
||||||
|
let sel = ScopedSelector::from_glob_mod(sel, source);
|
||||||
|
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||||
|
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sel,
|
||||||
|
css_ident,
|
||||||
|
html_ident,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct the [HashedSelector], with given hash and parsed class selector
|
||||||
|
pub fn from_class(hash: &ScopeHash, sel: &raffia::ast::ClassSelector) -> Self {
|
||||||
|
let sel = ScopedSelector::from_class(sel);
|
||||||
|
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||||
|
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sel,
|
||||||
|
css_ident,
|
||||||
|
html_ident,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct the [HashedSelector], with given hash and parsed id selector
|
||||||
|
pub fn from_id(hash: &ScopeHash, sel: &raffia::ast::IdSelector) -> Self {
|
||||||
|
let sel = ScopedSelector::from_id(sel);
|
||||||
|
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||||
|
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sel,
|
||||||
|
css_ident,
|
||||||
|
html_ident,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct the [HashedSelector], with given hash and parsed tagname selector
|
||||||
|
pub fn from_tag(hash: &ScopeHash, sel: &raffia::ast::TagNameSelector) -> Self {
|
||||||
|
let sel = ScopedSelector::from_tag(sel);
|
||||||
|
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||||
|
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sel,
|
||||||
|
css_ident,
|
||||||
|
html_ident,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform this selector into hashed version for CSS-ish language
|
||||||
|
///
|
||||||
|
/// Cooperates with the [HashedSelector::make_hashed_html], so
|
||||||
|
/// whats this function returns will be usable in HTML with to_hashed_html_def()
|
||||||
|
pub fn make_hashed_css(value: &ScopedSelector, hash: &ScopeHash) -> String {
|
||||||
|
match value {
|
||||||
|
ScopedSelector::Class(a) => {
|
||||||
|
// Class scoping is done by composition
|
||||||
|
format!("{}.{}", hash.as_str(), a.ident)
|
||||||
|
}
|
||||||
|
ScopedSelector::Tag(a) => {
|
||||||
|
// Tag scoping is done by composition
|
||||||
|
format!("{}.{}", a.ident, hash.as_str())
|
||||||
|
}
|
||||||
|
ScopedSelector::Id(a) => {
|
||||||
|
// ID scoping is done by class composition
|
||||||
|
format!("{}.{}", a.ident, hash.as_str())
|
||||||
|
}
|
||||||
|
ScopedSelector::Glob { raw, .. } => raw.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform this selector into hashed version valid for HTML-ish insertion
|
||||||
|
///
|
||||||
|
/// Cooperates with the [HashedSelector::make_hashed_css], so
|
||||||
|
/// whats this function returns will be usable in CSS with to_hashed_code()
|
||||||
|
///
|
||||||
|
/// Since 0.0.6 all scoping is done via classes, so this function
|
||||||
|
/// persists for bw-compat for everything except for class.
|
||||||
|
///
|
||||||
|
/// Global and Tag selectors are never transformed;
|
||||||
|
pub fn make_hashed_html(value: &ScopedSelector, hash: &ScopeHash) -> Option<String> {
|
||||||
|
match value {
|
||||||
|
// Class scoping is done with class composition
|
||||||
|
ScopedSelector::Class(a) => Some(format!("{} {}", a.ident, hash.as_str())),
|
||||||
|
ScopedSelector::Id(id) => Some(id.ident.clone()),
|
||||||
|
ScopedSelector::Tag(_) => None,
|
||||||
|
ScopedSelector::Glob { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Just any unary CSS-ish selector
|
||||||
|
#[derive(PartialEq, Eq, Hash)]
|
||||||
|
pub struct ArbitrarySelector {
|
||||||
|
/// CSS-ish identifier
|
||||||
|
pub ident: String,
|
||||||
|
/// Span in original CSS code where it comes from
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Any unary CSS-ish selector, that's able to be scoped, and then hashed
|
||||||
|
#[derive(PartialEq, Eq, Hash)]
|
||||||
|
pub enum ScopedSelector {
|
||||||
|
Class(ArbitrarySelector),
|
||||||
|
Id(ArbitrarySelector),
|
||||||
|
Tag(ArbitrarySelector),
|
||||||
|
Glob {
|
||||||
|
origin: ArbitrarySelector,
|
||||||
|
inner_span: Span,
|
||||||
|
raw: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScopedSelector {
|
||||||
|
/// Borrow the underlying [ArbitrarySelector]
|
||||||
|
pub fn as_arbitrary(&self) -> &ArbitrarySelector {
|
||||||
|
match self {
|
||||||
|
Self::Class(a) => a,
|
||||||
|
Self::Id(a) => a,
|
||||||
|
Self::Tag(a) => a,
|
||||||
|
Self::Glob { origin, .. } => origin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate rusty member ident based on selector type and CSS ident
|
||||||
|
pub fn gen_rusty_ident(&self) -> Option<syn::Ident> {
|
||||||
|
let arb = &self.as_arbitrary().ident;
|
||||||
|
let basic = sanitize_css_member(arb);
|
||||||
|
|
||||||
|
let ready = match self {
|
||||||
|
Self::Class(_) => Some(basic),
|
||||||
|
Self::Id(_) => Some(format!("the{basic}")),
|
||||||
|
Self::Tag(_) => Some(format!("any{basic}")),
|
||||||
|
Self::Glob { .. } => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
ready.map(|rs| {
|
||||||
|
syn::parse_str::<syn::Ident>(&rs).expect("BUG: formed ident is not Rust Ident")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct this from glob complex selector
|
||||||
|
pub fn from_glob_mod(s: (Span, Span), source: &str) -> Self {
|
||||||
|
let inner_span = s.1;
|
||||||
|
let origin_ident = source[s.0.start..s.0.end].to_owned();
|
||||||
|
|
||||||
|
Self::Glob {
|
||||||
|
origin: ArbitrarySelector {
|
||||||
|
span: s.0,
|
||||||
|
ident: origin_ident,
|
||||||
|
},
|
||||||
|
raw: source[inner_span.start..inner_span.end].to_owned(),
|
||||||
|
inner_span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct this from class selector
|
||||||
|
pub fn from_class(s: &raffia::ast::ClassSelector) -> Self {
|
||||||
|
let InterpolableIdent::Literal(lit) = &s.name else {
|
||||||
|
panic!("BUG: class selector was not a literal")
|
||||||
|
};
|
||||||
|
Self::Class(ArbitrarySelector {
|
||||||
|
ident: lit.raw.to_string(),
|
||||||
|
span: lit.span().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct this from id selector
|
||||||
|
pub fn from_id(s: &raffia::ast::IdSelector) -> Self {
|
||||||
|
let InterpolableIdent::Literal(lit) = &s.name else {
|
||||||
|
panic!("BUG: id selector was not a literal")
|
||||||
|
};
|
||||||
|
Self::Id(ArbitrarySelector {
|
||||||
|
ident: lit.raw.to_string(),
|
||||||
|
span: lit.span().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct this from tagname selector
|
||||||
|
pub fn from_tag(s: &raffia::ast::TagNameSelector) -> Self {
|
||||||
|
let InterpolableIdent::Literal(lit) = &s.name.name else {
|
||||||
|
panic!("BUG: tag selector was not a literal")
|
||||||
|
};
|
||||||
|
Self::Tag(ArbitrarySelector {
|
||||||
|
ident: lit.raw.to_string(),
|
||||||
|
span: lit.span().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_css_member(source: &str) -> String {
|
||||||
|
let mut target = String::with_capacity(source.len() + 1);
|
||||||
|
|
||||||
|
for part in source.split('-').filter(|p| !p.is_empty()) {
|
||||||
|
let mut chars = part.chars().filter(|c| c.is_ascii() && c.is_alphanumeric());
|
||||||
|
match (chars.next(), chars.next()) {
|
||||||
|
(None, None) => continue,
|
||||||
|
(Some(f), None) => {
|
||||||
|
target.push(f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
(Some(f), Some(s)) => {
|
||||||
|
target.push(f.to_ascii_uppercase());
|
||||||
|
target.push(s);
|
||||||
|
}
|
||||||
|
(None, Some(s)) => {
|
||||||
|
target.push(s);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chars.for_each(|c| target.push(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
match target.chars().next() {
|
||||||
|
Some(c) if c.is_numeric() => {
|
||||||
|
target.insert(0, 'n');
|
||||||
|
}
|
||||||
|
Some(c) => {
|
||||||
|
target.replace_range(..1, &c.to_ascii_lowercase().to_string());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
/* has to produce valid rust ident */
|
||||||
|
target.clear();
|
||||||
|
target.push_str("unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience wrapper for String-being-a-hash
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ScopeHash(String);
|
||||||
|
|
||||||
|
impl ScopeHash {
|
||||||
|
pub fn new(scope: &ArbitraryScope, config: &SabryHashConfig) -> Self {
|
||||||
|
use base64::Engine;
|
||||||
|
use raffia::ast::InterpolableIdent;
|
||||||
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
let mut hasher = std::hash::DefaultHasher::new();
|
||||||
|
|
||||||
|
if config.use_scope_name {
|
||||||
|
hasher.write(scope.name.to_string().as_bytes());
|
||||||
|
}
|
||||||
|
if config.use_code_text {
|
||||||
|
hasher.write(scope.adapter().source().as_bytes());
|
||||||
|
}
|
||||||
|
if config.use_code_size {
|
||||||
|
hasher.write(&scope.adapter().source().len().to_ne_bytes());
|
||||||
|
}
|
||||||
|
if config.use_item_names {
|
||||||
|
let classes = scope.adapter().class_selectors();
|
||||||
|
let ids = scope.adapter().id_selectors();
|
||||||
|
//# types/tags dont participate
|
||||||
|
//# child-parent dont participate
|
||||||
|
|
||||||
|
let merged_items = classes
|
||||||
|
.iter()
|
||||||
|
.map(|s| &s.name)
|
||||||
|
.chain(ids.iter().map(|i| &i.name))
|
||||||
|
.map(|ident| match ident {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.filter_map(|mbl| mbl.map(|l| l.raw))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
hasher.write(merged_items.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = hasher.finish().to_be_bytes();
|
||||||
|
|
||||||
|
let hash = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(hash);
|
||||||
|
let hash = sanitize_css_member(&hash);
|
||||||
|
|
||||||
|
Self(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn test_init(v: String) -> Self {
|
||||||
|
Self(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{
|
||||||
|
conf::SabryHashConfig,
|
||||||
|
style::{scope::HashedScope, syntax::SyntaxKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ScopeHash;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use syn::Ident;
|
||||||
|
|
||||||
|
use super::ArbitraryScope;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scope_hash_codegen() {
|
||||||
|
let code = ".cls1{color:red; &-dark{color: black;} #id1 {color:green;} div {color:blue;}} .cls3#id2{color: black;}";
|
||||||
|
let hash = ScopeHash::test_init("F2kf8nMs".into());
|
||||||
|
|
||||||
|
let expect_code = ".F2kf8nMs.cls1{color:red; &-dark{color: black;} #id1.F2kf8nMs {color:green;} div.F2kf8nMs {color:blue;}} .F2kf8nMs.cls3#id2.F2kf8nMs{color: black;}";
|
||||||
|
|
||||||
|
let expect_selector_htmls = HashSet::from([
|
||||||
|
"cls1".to_string(),
|
||||||
|
"id1".to_string(),
|
||||||
|
"cls3".to_string(),
|
||||||
|
"id2".to_string(),
|
||||||
|
"".to_string(), // comes from `div` which has no html hashed code, still want to check presense
|
||||||
|
]);
|
||||||
|
|
||||||
|
let scope =
|
||||||
|
ArbitraryScope::from_source(SyntaxKind::Scss, syn::parse_str("scope1").unwrap(), code)
|
||||||
|
.unwrap();
|
||||||
|
let scope = HashedScope::new(hash, scope).unwrap();
|
||||||
|
let scope_selector_htmls = scope
|
||||||
|
.hashed_selectors
|
||||||
|
.iter()
|
||||||
|
.map(|hs| hs.html_ident.clone().unwrap_or_default())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
// check that original is untouched
|
||||||
|
assert_eq!(code, scope.original_scope.adapter().source());
|
||||||
|
// check that hashed matches what we expect
|
||||||
|
assert_eq!(expect_code, scope.hashed_code);
|
||||||
|
// check that HTML selector hashed code matches what we expect
|
||||||
|
assert_eq!(expect_selector_htmls, scope_selector_htmls);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rusty_idents() {
|
||||||
|
let code = "
|
||||||
|
.cls-1{
|
||||||
|
color:red;
|
||||||
|
&-dark{
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
#id-1 {
|
||||||
|
color:green;
|
||||||
|
}
|
||||||
|
.-txt-of {
|
||||||
|
color:blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.-c_ls3#4id{
|
||||||
|
color: black;
|
||||||
|
}";
|
||||||
|
let expect_ident_names = HashSet::from([
|
||||||
|
"cls1".to_string(),
|
||||||
|
"theid1".to_string(),
|
||||||
|
"c_ls3".to_string(),
|
||||||
|
"then4id".to_string(),
|
||||||
|
"txtOf".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let hash_cfg = SabryHashConfig::default();
|
||||||
|
let scope = ArbitraryScope::from_source(
|
||||||
|
SyntaxKind::Scss,
|
||||||
|
syn::parse_str::<Ident>("scope2oiej").unwrap(),
|
||||||
|
code,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.hashed(&hash_cfg)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let sels = scope
|
||||||
|
.hashed_selectors
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.sel.gen_rusty_ident())
|
||||||
|
.flat_map(|i| i.map(|is| is.to_string()))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
assert_eq!(expect_ident_names, sels);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_matches() {
|
||||||
|
let source = ".cls1{color:red; &-dark{color: black} #id1 {color:green;} div {color:blue;}} .cls3#id2{color: black;}";
|
||||||
|
|
||||||
|
let scope1 = ArbitraryScope::from_source(
|
||||||
|
SyntaxKind::Scss,
|
||||||
|
syn::parse_str::<Ident>("lasifudm").unwrap(),
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let hash1 = ScopeHash::new(&scope1, &SabryHashConfig::default());
|
||||||
|
|
||||||
|
let scope2 = ArbitraryScope::from_source(
|
||||||
|
SyntaxKind::Scss,
|
||||||
|
syn::parse_str::<Ident>("lasifudm").unwrap(),
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let hash2 = ScopeHash::new(&scope2, &SabryHashConfig::default());
|
||||||
|
|
||||||
|
assert_eq!(hash1, hash2);
|
||||||
|
}
|
||||||
|
}
|
||||||
359
sabry_intrnl/src/style/syntax.rs
Normal file
359
sabry_intrnl/src/style/syntax.rs
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
use raffia::{
|
||||||
|
ParserBuilder,
|
||||||
|
ast::{
|
||||||
|
ClassSelector, ComplexSelectorChild, CompoundSelector, IdSelector, InterpolableIdent,
|
||||||
|
NestingSelector, PseudoClassSelector, SimpleSelector, Statement, Stylesheet, TypeSelector,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Convenience wrapper for [Stylesheet]
|
||||||
|
pub struct StylesheetAdapter<'s> {
|
||||||
|
pub syntax: SyntaxKind,
|
||||||
|
source: &'s str,
|
||||||
|
stylesheet: Stylesheet<'s>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> StylesheetAdapter<'s> {
|
||||||
|
pub fn new(source: &'s str, syntax: SyntaxKind) -> Result<Self, raffia::error::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
stylesheet: ParserBuilder::new(source)
|
||||||
|
.ignore_comments()
|
||||||
|
.syntax(syntax.into())
|
||||||
|
.build()
|
||||||
|
.parse()?,
|
||||||
|
syntax,
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(&self) -> &str {
|
||||||
|
self.source
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selectors(&self) -> Vec<CompoundSelector<'s>> {
|
||||||
|
Self::selectors_of(self.stylesheet.statements.iter().cloned()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_selectors(&self) -> Vec<ClassSelector<'s>> {
|
||||||
|
self.selectors_by(|c| match c {
|
||||||
|
SimpleSelector::Class(sel) => Some(sel),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id_selectors(&self) -> Vec<IdSelector<'s>> {
|
||||||
|
self.selectors_by(|c| match c {
|
||||||
|
SimpleSelector::Id(sel) => Some(sel),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_selectors(&self) -> Vec<TypeSelector<'s>> {
|
||||||
|
self.selectors_by(|c| match c {
|
||||||
|
SimpleSelector::Type(sel) => Some(sel),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nesting_selectors(&self) -> Vec<NestingSelector<'s>> {
|
||||||
|
self.selectors_by(|c| match c {
|
||||||
|
SimpleSelector::Nesting(sel) => Some(sel),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// special selector case searching for :g(whatever), :glob(whatever), :global(whatever)
|
||||||
|
pub fn glob_modified_selectors(&self) -> Vec<PseudoClassSelector<'s>> {
|
||||||
|
self.selectors_by(|c| {
|
||||||
|
let cps = match c {
|
||||||
|
SimpleSelector::PseudoClass(sel) => sel,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &cps.name {
|
||||||
|
InterpolableIdent::Literal(lit) => match lit.raw {
|
||||||
|
"g" => Some(cps),
|
||||||
|
"glob" => Some(cps),
|
||||||
|
"global" => Some(cps),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return collected [Vec] of owned values modulo `T`, filtered by `F`.
|
||||||
|
///
|
||||||
|
/// This is preferred way to apply custom filtering on selectors over
|
||||||
|
/// `selectors().iter().filter_map()`
|
||||||
|
/// as it does not allocate intermediate vector
|
||||||
|
pub fn selectors_by<T, F>(&self, form: F) -> Vec<T>
|
||||||
|
where
|
||||||
|
F: FnMut(SimpleSelector<'s>) -> Option<T>,
|
||||||
|
{
|
||||||
|
Self::selectors_of(self.stylesheet.statements.iter().cloned())
|
||||||
|
.flat_map(|cs| cs.children)
|
||||||
|
.filter_map(form)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selectors_of(
|
||||||
|
c: impl Iterator<Item = Statement<'s>>,
|
||||||
|
) -> impl Iterator<Item = CompoundSelector<'s>> {
|
||||||
|
// Hello. Its Dan.
|
||||||
|
// Im not proud of this,
|
||||||
|
// but more effective implementation
|
||||||
|
// was SO unreadable and SO infected with lifetimes
|
||||||
|
// I suck at those things
|
||||||
|
|
||||||
|
// For everyone trying to make this MORE OPTIMAL:
|
||||||
|
// increment the hour-counter and post your name here:
|
||||||
|
// 4 - Danik
|
||||||
|
// 5 - Yiffy
|
||||||
|
// 8 - Danik
|
||||||
|
c.filter_map(|s| match s {
|
||||||
|
Statement::QualifiedRule(stmt) => Some(stmt),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flat_map(|q| {
|
||||||
|
let thissels = q
|
||||||
|
.selector
|
||||||
|
.selectors
|
||||||
|
.iter()
|
||||||
|
.flat_map(|sel| &sel.children)
|
||||||
|
.filter_map(|sc| match sc {
|
||||||
|
ComplexSelectorChild::CompoundSelector(sel) => Some(sel),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let blockstmt = q.block.statements.iter().cloned();
|
||||||
|
Self::selectors_of(blockstmt)
|
||||||
|
.chain(thissels)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience unification entrypoint
|
||||||
|
/// to make friends with all the different syntax-consumers
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
pub enum SyntaxKind {
|
||||||
|
#[default]
|
||||||
|
Scss,
|
||||||
|
Sass,
|
||||||
|
Css,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for SyntaxKind {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"sass" => Ok(Self::Sass),
|
||||||
|
"scss" => Ok(Self::Scss),
|
||||||
|
"css" => Ok(Self::Css),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<raffia::Syntax> for SyntaxKind {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: raffia::Syntax) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
raffia::Syntax::Sass => Ok(Self::Sass),
|
||||||
|
raffia::Syntax::Scss => Ok(Self::Scss),
|
||||||
|
raffia::Syntax::Css => Ok(Self::Css),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "style-compiler")]
|
||||||
|
impl From<sasso::Syntax> for SyntaxKind {
|
||||||
|
fn from(value: sasso::Syntax) -> Self {
|
||||||
|
match value {
|
||||||
|
sasso::Syntax::Sass => Self::Sass,
|
||||||
|
sasso::Syntax::Scss => Self::Scss,
|
||||||
|
sasso::Syntax::Css => Self::Css,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SyntaxKind> for raffia::Syntax {
|
||||||
|
fn from(value: SyntaxKind) -> Self {
|
||||||
|
match value {
|
||||||
|
SyntaxKind::Sass => Self::Sass,
|
||||||
|
SyntaxKind::Scss => Self::Scss,
|
||||||
|
SyntaxKind::Css => Self::Css,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "style-compiler")]
|
||||||
|
impl From<SyntaxKind> for sasso::Syntax {
|
||||||
|
fn from(value: SyntaxKind) -> Self {
|
||||||
|
match value {
|
||||||
|
SyntaxKind::Scss => Self::Scss,
|
||||||
|
SyntaxKind::Sass => Self::Sass,
|
||||||
|
SyntaxKind::Css => Self::Css,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use raffia::{
|
||||||
|
Spanned,
|
||||||
|
ast::{InterpolableIdent, TypeSelector},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::style::syntax::SyntaxKind;
|
||||||
|
|
||||||
|
use super::StylesheetAdapter;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sels() {
|
||||||
|
let source_scss = "
|
||||||
|
.r1{
|
||||||
|
.r2.r3 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
&-rod {
|
||||||
|
color: red;
|
||||||
|
&[value] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div#id2 {
|
||||||
|
& > span.seled {
|
||||||
|
color: red;
|
||||||
|
& + ul#id2[attr] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
& + .uled[attr] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&::after{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
";
|
||||||
|
let source_sass = "
|
||||||
|
.r1
|
||||||
|
.r2.r3
|
||||||
|
color: red
|
||||||
|
&-rod
|
||||||
|
color: red
|
||||||
|
&[value]
|
||||||
|
color: red
|
||||||
|
div#id2
|
||||||
|
& > span.seled
|
||||||
|
color: red
|
||||||
|
& + ul#id2[attr]
|
||||||
|
color: red
|
||||||
|
& + .uled[attr]
|
||||||
|
color: red
|
||||||
|
&::after
|
||||||
|
color: red
|
||||||
|
";
|
||||||
|
let expect_classes = HashSet::from(["r2", "r3", "r1", "uled", "seled"]);
|
||||||
|
let expect_ids = HashSet::from(["id2", "id2"]);
|
||||||
|
let expect_tags = HashSet::from(["ul", "div", "span"]);
|
||||||
|
|
||||||
|
//scss
|
||||||
|
let adp = StylesheetAdapter::new(source_scss, SyntaxKind::Scss).unwrap();
|
||||||
|
|
||||||
|
let classes = adp
|
||||||
|
.class_selectors()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| match &c.name {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit.raw),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let ids = adp
|
||||||
|
.id_selectors()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| match &i.name {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit.raw),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let tags = adp
|
||||||
|
.type_selectors()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| match t {
|
||||||
|
TypeSelector::TagName(tag) => Some(tag),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.filter_map(|t| match &t.name.name {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit.raw),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
assert_eq!(expect_classes, classes);
|
||||||
|
assert_eq!(expect_ids, ids);
|
||||||
|
assert_eq!(expect_tags, tags);
|
||||||
|
|
||||||
|
//sass
|
||||||
|
let adp = StylesheetAdapter::new(source_sass, SyntaxKind::Sass).unwrap();
|
||||||
|
|
||||||
|
let classes = adp
|
||||||
|
.class_selectors()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| match &c.name {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit.raw),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let ids = adp
|
||||||
|
.id_selectors()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| match &i.name {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit.raw),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let tags = adp
|
||||||
|
.type_selectors()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| match t {
|
||||||
|
TypeSelector::TagName(tag) => Some(tag),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.filter_map(|t| match &t.name.name {
|
||||||
|
InterpolableIdent::Literal(lit) => Some(lit.raw),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
assert_eq!(expect_classes, classes);
|
||||||
|
assert_eq!(expect_ids, ids);
|
||||||
|
assert_eq!(expect_tags, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_class() {
|
||||||
|
let scss = ".cls1{color:red; .cls2{color:green;}}";
|
||||||
|
let newname = "cls203-jg7ihgjhftyfhjh";
|
||||||
|
let expect = ".cls1{color:red; .cls203-jg7ihgjhftyfhjh{color:green;}}";
|
||||||
|
|
||||||
|
let adp = StylesheetAdapter::new(scss, SyntaxKind::Scss).unwrap();
|
||||||
|
let classes = adp.class_selectors();
|
||||||
|
let target_class = classes[0].clone();
|
||||||
|
|
||||||
|
let span = target_class.name.span();
|
||||||
|
let renamed = format!("{}{}{}", &scss[..span.start], newname, &scss[span.end..]);
|
||||||
|
assert_eq!(renamed, expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
/// Convenience unification entrypoint
|
|
||||||
/// to make friends with all the different syntax-consumers
|
|
||||||
#[derive(Default, Debug, Clone, Copy)]
|
|
||||||
pub enum OneSyntaxToRuleThemAll {
|
|
||||||
#[default]
|
|
||||||
Scss,
|
|
||||||
Sass,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for OneSyntaxToRuleThemAll {
|
|
||||||
type Error = ();
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
"sass" => Ok(Self::Sass),
|
|
||||||
"scss" => Ok(Self::Scss),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<raffia::Syntax> for OneSyntaxToRuleThemAll {
|
|
||||||
type Error = ();
|
|
||||||
fn try_from(value: raffia::Syntax) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
raffia::Syntax::Sass => Ok(Self::Sass),
|
|
||||||
raffia::Syntax::Scss => Ok(Self::Scss),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
impl TryFrom<grass::InputSyntax> for OneSyntaxToRuleThemAll {
|
|
||||||
type Error = ();
|
|
||||||
fn try_from(value: grass::InputSyntax) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
grass::InputSyntax::Sass => Ok(Self::Sass),
|
|
||||||
grass::InputSyntax::Scss => Ok(Self::Scss),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OneSyntaxToRuleThemAll> for raffia::Syntax {
|
|
||||||
fn from(value: OneSyntaxToRuleThemAll) -> Self {
|
|
||||||
match value {
|
|
||||||
OneSyntaxToRuleThemAll::Sass => Self::Sass,
|
|
||||||
OneSyntaxToRuleThemAll::Scss => Self::Scss,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "compile")]
|
|
||||||
impl From<OneSyntaxToRuleThemAll> for grass::InputSyntax {
|
|
||||||
fn from(value: OneSyntaxToRuleThemAll) -> Self {
|
|
||||||
match value {
|
|
||||||
OneSyntaxToRuleThemAll::Scss => Self::Scss,
|
|
||||||
OneSyntaxToRuleThemAll::Sass => Self::Sass,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue