Merge pull request #1 from yiffyrusdev/window

Window merge (lepty-scoping)
This commit is contained in:
Yiffy Dev 2024-10-31 09:17:51 +05:00 • committed by GitHub
commit 258d362c0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 255 additions and 98 deletions

View file

@ -6,7 +6,7 @@ on:
pull_request:
branches: ["window"]
env:
env:
CARGO_TERM_COLOR: always
jobs:
@ -22,4 +22,5 @@ jobs:
- uses: actions/checkout@v4
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose --all
- run: cargo test --verbose --all --all-features

1
Cargo.lock generated
View file

@ -1012,6 +1012,7 @@ version = "0.0.1-alpha.1"
dependencies = [
"base64",
"blake3",
"cfg-if",
"const_format",
"grass",
"lightningcss",

View file

@ -43,6 +43,9 @@ sabry_intrnl = {workspace = true}
[features]
default = ["procmacro"]
lepty-scoping = [
"sabry_intrnl/lepty-scoping"
]
procmacro = [
"dep:sabry_procmacro"
]

21
LICENSE
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Svyatoslav Pushkarev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -12,6 +12,10 @@
>
> the latest-trashy state is in the "window" branch
[![Crates.io](https://img.shields.io/crates/v/sabry.svg)](https://crates.io/crates/sabry)
[![Docs.rs](https://img.shields.io/docsrs/sabry/latest.svg)](https://docs.rs/sabry)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yiffyrusdev/sabry/blob/master/LICENSE)
At first, I'll show how this crate "tastes". With SABRY, its in your power to:
<table>
@ -65,7 +69,7 @@ tgk_brandstyle = {version = "0.0.1", features = ["utils"]}
`@use` your style-crates in sass code *naturally*
```rust,ignore
styly!(breadcumb {
styly!(breadbadge {
@use 'tokens';
.scope {@include tokens.badge(primary);}
})
@ -130,7 +134,7 @@ Also, just about everything is pub-available in this crate - Sabry is ready for
Feel free to check out examples:
|[crate of styles](./examples//define-styles/)|[style usage](./examples/use-styles/)|[leptox-axum with sabry](./examples/leptos-axum)|
|[crate of styles](./examples//define-styles/)|[style usage](./examples/use-styles/)|[leptos-axum with sabry](./examples/leptos-axum)|
|-|-|-|
### Create a crate full of arbitrary SASS
@ -176,11 +180,11 @@ sabry = {version = "0.0.1"}
```
And create a style scope wherever you want:
```rust,ignore
// breadcumblist.rs
// breadbadgelist.rs
use sabry::styly;
styly!(const styles {
.cumbs {
.badges {
display: flex;
&__list {
display: flex;
@ -192,8 +196,8 @@ styly!(const styles {
});
fn render() -> HtmlElement {
div().class(styles::cumbs).chain(
ul().class(styles::_list(styles::cumbs))
div().class(styles::badges).chain(
ul().class(styles::_list(styles::badges))
).chain(
span().id(styles::thewolf)
)
@ -201,10 +205,12 @@ fn render() -> HtmlElement {
```
> That `const` usage is covered in the [following](#constant-styly-scopes) section. In this example we dont invoke sabry build-magic, so, to be as close to real life as possible, I used a const.
Every selector, if that does make sense, now available for you as a member of `styles` scope. In this example - `styles::cumbs`, `styles::thewolf` and `styles::_list()`. More about scoping and member names you can read [here](#styly-scopes).
Every selector, if that does make sense, now available for you as a member of `styles` scope. In this example - `styles::badges`, `styles::thewolf` and `styles::_list()`. More about scoping and member names you can read [here](#styly-scopes).
### Use styles earlier created in another crate
> **Tip** | If you use [leptos](https://github.com/leptos-rs/leptos) - check [this](#leptos-specials) section as well
The combination of previous two, with some additional work to do and some extra sugar to enjoy.
Sabry is needed as both dependency and build-dependency.
@ -238,12 +244,12 @@ fn main(){
Now lets get back to the code and use the mixin defined in another crate:
```rust,ignore
// breadcumblist.rs
// breadbadgelist.rs
use sabry::styly;
styly!(styles {
@use "mixins";
.cumbs {
.badges {
display: flex;
&__list {
display: flex;
@ -255,8 +261,8 @@ styly!(styles {
});
fn render() -> HtmlElement {
div().class(styles::cumbs).chain(
ul().class(styles::_list(styles::cumbs))
div().class(styles::badges).chain(
ul().class(styles::_list(styles::badges))
).chain(
span().id(styles::thewolf)
)
@ -264,6 +270,73 @@ fn render() -> HtmlElement {
```
So the `mixins!` macro we just passed to the `usey!` macro inside of `buildy` function call is now accessible with simple and natural `@use "mixins"` SASS rule!
### Leptos specials
With [leptos](https://github.com/leptos-rs/leptos) framework you can do the trick to apply some class to most of HtmlElements for the entire component/island:
```rust,ignore
#[component]
fn component() -> impl IntoView {
view! {class="cls1", /* <- here */
<h1>"Head"</h1>
<p>"text"</p>
}
}
```
... and have "cls1" on both `h1` and `p` auto-assigned. Isn't this cool?
So, if you're on leptos, and don't mind to take this approach, I'd highly recommend turning the *lepty-scoping* feature on:
```toml
# Cargo.toml
[dependencies]
sabry = {version = "0.0.1", features = ["lepty-scoping"]}
[build-dependencies]
sabry = {version = "0.0.1", features = ["build", "lepty-scoping"]}
```
```rust,ignore
styly!(scope {
@use "tokens";
h1 {
@include tokens.sectionhead();
img {
@include tokens.sectionimg();
}
}
.breadcumbs {
@include tokens.badgelist();
&__item {
@include tokens.badge(secondary);
}
}
});
#[component]
fn component() -> impl IntoView {
view! {class=SCOPE,
<h1>
"Head"
<img src="whatever"/>
</h1>
<ul class=scope::breadcumbs>
<li class=scope::___item(scope::breadcumbs)>
"Home page"
</li>
</ul>
}
}
```
That will perform *much* better:
- All scope members - like `scope::breadcumbs` - wont contain repeating scope hash, just the original class/id selector
- You will not encounter [the catch](#scoping) with nested tagname selectors
Also, this isn't really exclusive leptos-supporting feature. It just changes scoping behavour.
## Configuration
Sabry configuration lives in `[package.metadata.sabry]` table of the manifest file.
@ -531,6 +604,8 @@ Different selector types are scoped differently:
>
> Also if there are nested tagname selectors - sabry won't take this into account and you'll
> have to wrap them with `SCOPE` class as well
>
> **However** If you are able to apply some class to all html elements you have - like `view!{class=CLASS...}` with Leptos - you could use *lepty-scoping* feature flag for sabry and get rid of this catch! See more [here](#leptos-specials).
As for **SASS parent selectors**: they are currently handled in different way. Instead of
walking up the syntax tree sabry just creates function member for the scope and leave the rest to grass:

View file

@ -994,28 +994,15 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "sabry"
version = "0.0.1"
version = "0.0.1-alpha.1"
dependencies = [
"sabry_build",
"sabry_intrnl",
"sabry_procmacro",
]
[[package]]
name = "sabry_build"
version = "0.0.0"
dependencies = [
"proc-macro2",
"sabry_intrnl",
"sabry_procmacro_impl",
"syn 2.0.85",
"thiserror",
"walkdir",
]
[[package]]
name = "sabry_intrnl"
version = "0.0.0"
version = "0.0.1-alpha.1"
dependencies = [
"base64",
"blake3",
@ -1023,6 +1010,7 @@ dependencies = [
"grass",
"lightningcss",
"proc-macro2",
"quote",
"raffia",
"regex",
"serde",
@ -1033,14 +1021,14 @@ dependencies = [
[[package]]
name = "sabry_procmacro"
version = "0.0.0"
version = "0.0.1-alpha.1"
dependencies = [
"sabry_procmacro_impl",
]
[[package]]
name = "sabry_procmacro_impl"
version = "0.0.0"
version = "0.0.1-alpha.1"
dependencies = [
"proc-macro2",
"quote",
@ -1050,15 +1038,6 @@ dependencies = [
"syn 2.0.85",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -1290,16 +1269,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1361,15 +1330,6 @@ version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"

View file

@ -1,5 +1,7 @@
use sabry::sassy;
sassy!(style "src/style.scss");
sassy!(utils {
@mixin flex($direction, $align, $justify, $gap: .5rem) {
display: flex;

View file

@ -0,0 +1,5 @@
@use "theme" as *;
.codeblock {
background: $thm-surface-primary;
}

View file

@ -23,11 +23,11 @@ tracing = { version = "0.1", optional = true }
http = "1"
# sabry with sass
sabry = {path = "../../"}
sabry = {path = "../../", features = ["lepty-scoping"]}
[build-dependencies]
# sabry for build with sass
sabry = {path = "../../", features = ["build"]}
sabry = {path = "../../", features = ["build", "lepty-scoping"]}
# lets depend on our styles
define-styles = {path = "../define-styles"}
@ -51,11 +51,12 @@ css.minify = true
css.bundle = "target/.sabry/target/bundle.css"
[package.metadata.leptos]
# Sabry-specific quality of life config
style-file = "target/.sabry/target/bundle.css"
output-name = "leptos-axum"
site-root = "target/site"
site-pkg-dir = "pkg"
# Sabry-generated bundle for leptos
style-file = "target/.sabry/target/bundle.css"
assets-dir = "public"
site-addr = "127.0.0.1:3000"
reload-port = 3001

View file

@ -1,10 +1,32 @@
use leptos::prelude::*;
use sabry::styly;
styly!(s "src/routes/home.scss");
styly!(s2 {
styly!(style {
@use "tokens";
@use "utils";
@use "theme";
h2 {
margin-bottom: 1rem;
@include theme.surface(primary);
@include theme.txt(primary);
span {
@include theme.txt(accent);
}
}
.btn {
@include tokens.clickable(accent);
&-dark {
@include tokens.clickable(secondary, 0.8, 1.1);
}
}
.table {
@include utils.flex(column, center, center);
@include theme.surface(primary);
}
.card {
@include theme.surface(secondary);
}
@ -12,6 +34,7 @@ styly!(s2 {
@include theme.surface(secondary);
text-transform: uppercase;
}
});
/// Renders the home page of your application.
@ -21,13 +44,20 @@ pub fn Route() -> impl IntoView {
let count = RwSignal::new(0);
let on_click = move |_| *count.write() += 1;
view! {
<section class=s::table>
<h1>"Welcome to Leptos!"</h1>
<button class=s::btn on:click=on_click>"Ima CRAZY button, clicked: " {count} " times!"</button>
<span class=s2::card>"DISCLAIMER:"</span>
<p class=s2::pocwarn>"This isn't a design example! Its just proof of sabry's capabilities."</p>
<p class=s2::pocwarn>"Plz dont make websites/apps which look like this page :D"</p>
view! {class = STYLE,
<section class=style::table>
<h2>"Welcome to Leptos"</h2>
<h2 class="">
"This head is styled with lepty-scopes feature"
<br/>
<span class="">"And this child too!"</span>
</h2>
<button class=style::btn on:click=on_click>"Ima CRAZY button, clicked: " {count} " times!"</button>
<button class=style::_dark(style::btn) on:click=on_click>"Ima CRAZY button, clicked: " {count} " times!"</button>
<span class=style::card>"DISCLAIMER:"</span>
<p class=style::pocwarn>"This isn't a design example! Its just proof of sabry's capabilities."</p>
<p class=style::pocwarn>"Plz dont make websites/apps which look like this page :D"</p>
</section>
}
}

View file

@ -1,12 +0,0 @@
@use "tokens";
@use "utils";
@use "theme";
.btn {
@include tokens.clickable(accent);
}
.table {
@include utils.flex(column, center, center);
@include theme.surface(primary);
}

View file

@ -23,5 +23,7 @@ const_format = {version = "^0.2"}
thiserror = {version = "^1.0"}
serde = {version = "^1.0", features = ["derive"]}
toml = {version = "^0.8"}
cfg-if = {version = "^1.0"}
[features]
lepty-scoping = []

View file

@ -1,3 +1,4 @@
use cfg_if::cfg_if;
use hash::ScopeHash;
use raffia::Spanned;
use regex::Regex;
@ -187,10 +188,22 @@ impl HashedSelector {
// As we dont want to use two HTML props for a single ID,
// ID scoping is done with id modification
ScopedSelector::Id(a) => {
format!("{}-{}", hash.as_str(), a.ident)
cfg_if! {
if #[cfg(feature = "lepty-scoping")] {
format!("{}.{}", a.ident, hash.as_str())
} else {
format!("{}-{}", hash.as_str(), a.ident)
}
}
}
ScopedSelector::Tag(a) => {
format!(".{} {}", hash.as_str(), a.ident)
cfg_if! {
if #[cfg(feature = "lepty-scoping")] {
format!("{}.{}", a.ident, hash.as_str())
} else {
format!(".{} {}", hash.as_str(), a.ident)
}
}
}
}
}
@ -205,10 +218,26 @@ impl HashedSelector {
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!("{} {}", hash.as_str(), a.ident)),
ScopedSelector::Class(a) => {
cfg_if! {
if #[cfg(feature = "lepty-scoping")] {
Some(a.ident.to_string())
} else {
Some(format!("{} {}", hash.as_str(), a.ident))
}
}
},
// As we dont want to use two HTML props for a single ID,
// ID scoping is done with id modification
ScopedSelector::Id(a) => Some(format!("{}-{}", hash.as_str(), a.ident)),
ScopedSelector::Id(a) => {
cfg_if! {
if #[cfg(feature = "lepty-scoping")] {
Some(a.ident.to_string())
} else {
Some(format!("{}-{}", hash.as_str(), a.ident))
}
}
},
ScopedSelector::Tag(_) => None,
}
}
@ -330,6 +359,7 @@ pub fn apply_basic_rusty_member_gen_rules(source: &str) -> String {
mod test {
use std::collections::HashSet;
use cfg_if::cfg_if;
use syn::Ident;
use crate::{
@ -345,14 +375,33 @@ mod test {
".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;} #F2kf8nMs-id1 {color:green;} .F2kf8nMs div {color:blue;}} .F2kf8nMs.cls3#F2kf8nMs-id2{color: black;}";
let expect_selector_htmls = HashSet::from([
"F2kf8nMs cls1".to_string(),
"F2kf8nMs-id1".to_string(),
"F2kf8nMs cls3".to_string(),
"F2kf8nMs-id2".to_string(),
"".to_string(), // comes from `div` which has no html hashed code, still want to check presense
]);
cfg_if! {
if #[cfg(feature = "lepty-scoping")]{
let expect_code = ".F2kf8nMs.cls1{color:red; &-dark{color: black;} #id1.F2kf8nMs {color:green;} div.F2kf8nMs {color:blue;}} .F2kf8nMs.cls3#id2.F2kf8nMs{color: black;}";
} else {
let expect_code = ".F2kf8nMs.cls1{color:red; &-dark{color: black;} #F2kf8nMs-id1 {color:green;} .F2kf8nMs div {color:blue;}} .F2kf8nMs.cls3#F2kf8nMs-id2{color: black;}";
}
}
cfg_if! {
if #[cfg(feature = "lepty-scoping")] {
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
]);
} else {
let expect_selector_htmls = HashSet::from([
"F2kf8nMs cls1".to_string(),
"F2kf8nMs-id1".to_string(),
"F2kf8nMs cls3".to_string(),
"F2kf8nMs-id2".to_string(),
"".to_string(), // comes from `div` which has no html hashed code, still want to check presense
]);
}
}
let scope = ArbitraryScope::from_source(
raffia::Syntax::Scss,

View file

@ -1,4 +1,4 @@
use std::{fmt::Debug, fs};
use std::{fmt::Debug, fs, path::PathBuf, str::FromStr};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
@ -36,9 +36,28 @@ impl Parse for ArbitraryStyleBlock {
let (code, span) = if let Ok(path_tok) = input.parse::<LitStr>() {
let path = path_tok.value();
let iofile = match fs::read(&path) {
let path = match PathBuf::from_str(&path) {
Ok(p) => p,
Err(_) => {
return Err(syn::Error::new(
path_tok.span(),
format!("Could not read {} as a path: Infallible", path),
))
}
};
let fullpath = match path.canonicalize() {
Ok(cp) => cp,
Err(e) => {
return Err(syn::Error::new(
path_tok.span(),
format!("Could not use path {path:?}: {:?}", e.kind()),
))
}
};
let iofile = match fs::read(&fullpath) {
Ok(bf) => bf,
Err(_) => return Err(syn::Error::new(path_tok.span(), format!("Could not read file '{path}'. Perhaps its relative? Thats not currently supported :(")))
Err(_) => return Err(syn::Error::new(path_tok.span(), format!("Could not read file at {fullpath:?}")))
};
let code = String::from_utf8_lossy(&iofile).to_string();