# 🧙🏻 SABRY - Syntactically Awesome, But RustY
**Y**et another **R**usty **B**oilerplate-free **A**gnostic **S**tyling crate, which brings your SASS/SCSS style into Rust. Written by a fox this time.
\* sabry isn't "syntactically awesome", it refers to SASS abbr expansion.
> **Project status** - early, my team uses it now and again in production, and the most of features/fixes do come on demand. I'm pretty happy with ergonomics and taste of the crate, and I'll do my best to keep DX the way it is between minor versions - but there's no guarantee on backwards-compatibility and no refunds if something breaks.
>
> "master" branch is what's currently on crates.io
>
> each version ever been on crates.io (except the latest release) is available in "vX.X.X" branch
>
> the latest-trashy state is in the "window" branch
[](https://crates.io/crates/sabry)
[](https://docs.rs/sabry)
[](https://github.com/yiffyrusdev/sabry/blob/master/LICENSE)
At first, I'll show how this crate "tastes". With SABRY, its in your power to:
<hr/>
Write arbitrary SASS/SCSS code and ship it *as a crate* however you please: with modules, with feature flags, etc. Say "bye" to manual copying, cli-tools and consts.
```rust
sabry::scssy!(tokens {"@mixin colored{color: red;}"});
```
<hr/>
Get the *(almost)* proper autocompletion for your scope from rust-analyzer
```rust
sabry::styly!(scope {".cls1{} #id1{}"});
/* autocompleted for you */
scope::cls1;
scope::theid1;
```
<hr/>
Code your SASS in separate files to get proper syntax highlighting, or do sass-in-the-rust to keep things in one place
```rust
sabry::styly!(component {".btn {color: green;}"});
sabry::styly!(extras "tests/assets/mixin-module.scss");
```
<hr/>
Depend on styles with cargo, at build time, which brings all the rusty sweeties in: versions, updates, cratesio, local registries, workspaces, etc.
```toml
[build-dependencies.your_styles]
registry = "your_registry_isnt_that_cool"
version = "^0.1"
features = ["darkmode", "mobile"]
```
<hr/>
`@use` your style-crates in sass code *naturally*
```rust
sabry::styly!(breadbadge {"
@use 'tokens';
.scope {@include tokens.badge(primary);}
"});
```
<hr/>
Keep things as private and modular as you wish
```rust
sabry::styly!(pub cats "tests/assets/mixin-module.scss");
sabry::styly!(dogs {"#howl {border: none;}"});
```
```rust,ignore
/// something like render function
{
<div class={cats::meow}>
<ul id={dogs::thehowl}></ul>
</div>
}
```
<hr/>
Compile all the sweet SASS/SCSS into the optimized CSS bundle, ship it in CSS shunks or even include the compiled style into the binary
```rust
sabry::styly!(cssbundle {".c1 {color: white;}"});
sabry::styly!(const binary {".c2 {color: black;}"});
```
<hr/>
Sabry will gladly:
- scope and hash your styles, so they won't conflict, with amazing [raffia](https://crates.io/crates/raffia) parser
- compile SASS/SCSS into CSS with [grass](https://crates.io/crates/grass)
- optimize produced CSS with [lightningcss](https://crates.io/crates/lightningcss)
- prepare ready CSS bundle and put it wherever you wish
- include CSS into the build artifact - if you really want it
Also, just about everything is pub-available in this crate (with *internals* feature flag) - Sabry is ready for experiments.
## Usage
Feel free to check out examples:
### Create a crate full of arbitrary SASS
The only need is the dependency
```toml
#Cargo.toml
[dependencies]
sabry = {version = "0.0.5"}
```
And a proc-macro
```rust
// lib.rs
use sabry::scssy;
scssy!(mixins "tests/assets/mixin-module.scss");
scssy!(styles {"
$primary-color: black;
@mixin colored($col: primary) {
@if $col == primary {
color: $primary-color;
} @else {
color: $col;
}
}
"});
```
*\* Unlike most of other crates that do sass-in-the-rust, sabry currently does not allow unquoted sass/scss. You still have to write it in a string-quotes. Unquoted sass/scss is reserved for the future, where, hopefully, I'll find more exciting usage for it. In the meantime it does look like Zed, for example, still highlights your code (:*
Now you can build and see two ready for export macros: `mixins!` and `styles!`.
These are usefull on their own, as invocation of `mixins!()` or `styles!()` - both shall give you the code literal.
However there's more sweet use case for them, which is covered below.
### Write scoped styles
Depend on sabry
```toml
# Cargo.toml
[dependencies]
sabry = {version = "0.0.5"}
```
And create a style scope wherever you want:
```rust
// breadbadgelist.rs
use sabry::styly;
styly!(const styles {"
.badges {
display: flex;
&__list {
display: flex;
}
}
#wolf {
color: white;
}
"});
/* Now you can use all scoped selectors: */
let badges_scoped_class = styles::badges;
let wolf_scoped_id = styles::thewolf;
let badges__list_scoped_class = styles::___list(styles::badges);
```
> 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::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.
To be able to compile all styles sabry also needs the *build* feature flag:
```toml
# Cargo.toml
[dependencies]
sabry = {version = "0.0.5"}
[build-dependencies]
sabry = {version = "0.0.5", features = ["build"]}
```
> If you do use some non-default feature flags make sure to keep them in sync between sabry-dependency and sabry-build-dependency.
Then you have to tell sabry when code should be compiled. We'll do this in *build.rs* file.
```rust,ignore
// build.rs
fn main(){
sabry::buildy(
sabry::usey!(
mixins!(),
styles!()
)
).expect("Failed to build sabry styles");
}
```
`buildy` is the entry function of sabry build-time process. The handy `usey!` macro will do just proper handling of our style-macros for it.
Now lets get back to the code and use the mixin defined in another crate:
```rust
// breadbadgelist.rs
use sabry::styly;
styly!(styles {"
// 'mixins' is available beacuse we did call mixins!() macro in the example below
@use 'mixins';
.badges {
display: flex;
&__list {
display: flex;
}
}
#wolf {
@include mixins.colored(white);
}
"});
```
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!
### Create crate of styled components
This is currently an alpha-testing-early-concept feature which comes
with some things to avoid.
While the process depends much on the framework you prefer, there are
some limitations and essential recommendations:
- Sabry will scope (hash) your styles at build time, which means
higher collision probability between the component crate and the main application, which *can not* be currently
detected by sabry.
- You have to write style scopes for components with `const` styly macro flavour: `styly!(const whatever {""})`
- And then you have to solve the task of injecting those generated CSS for your component manually.
- If you want to write styles in separate SASS/SCSS files, you need the nightly rust and the 'nightly' feature flag set
for sabry, so you can use styles from relative paths `styly!(const comp "./style.scss")`
- Which will give you an error from rust-analyzer wether file exists or not. However, if the path is correct, it will build fine.
<details>
<summary>I'd suggest something like this</summary>
```rust,ignore
// lib.rs
pub mod utils;
pub mod form;
pub const fn css() -> &'static str {
concatcp!(utils::css(), form::css())
}
```
```rust,ignore
// utils.rs
use sabry::styly;
styly!(pub const scope:scss {"
.whatever {
&__code {}
&[you-please] {}
}
"});
styly!(pub const another:scss {"
.whatever {
&__code {}
&[you-please] {}
}
"});
pub const fn css() -> &'static str {
concatcp!(SCOPE, ANOTHER)
}
```
```rust,ignore
// form.rs
use sabry::styly;
styly!(pub const scope:scss {"
.whatever {
&__code {}
&[you-please] {}
}
"});
styly!(pub const another:scss {"
.whatever {
&__code {}
&[you-please] {}
}
"});
pub const fn css() -> &'static str {
concatcp!(SCOPE, ANOTHER)
}
```
So I'd say - do the const function back-propogation and let the consumer decide how to include the CSS of the component crate.
</details>
### 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
use leptos::prelude::*;
#[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 that 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.5", features = ["lepty-scoping"]}
[build-dependencies]
sabry = {version = "0.0.5", features = ["build", "lepty-scoping"]}
```
```rust
use leptos::prelude::*;
use sabry::styly;
styly!(scope {"
// assuming we have tokens!() macro generated by `scssy!`, which is handled by `buildy`
@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="breadcumbs">
<li class="breadcumbs__item">
"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
- So instead of scope members (which [aren't](https://github.com/yiffyrusdev/sabry/issues/2) autocompleted for sass-in-the-rust yet) you could just write bare selectors, as you do, for example, with svelte: `<li class="breadcumbs__item">`
- You'll still have the rusty mod generated: its just the `scope::breadcumbs` is the `"breadcumbs"` without the hash
- 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.
All configurations are optional, but with default configuration sabry won't produce any CSS files.
Full example, close to defaults:
```toml
# Cargo.toml
[package.metadata.sabry]
css.bundle = "target/static/style.css"
css.prelude = ["assets/prelude.css"]
css.scopes = "target/statis/scopes/"
css.minify = true
sass.intermediate_dir = "target/.sabry/sass"
sass.module_name_collision = "merge"
sass.modules = ["assets/sass/mod1.scss"]
sass.prelude = ["assets/sass/prelude.scss"]
sass.scanroot = "src"
hash.size = 6
hash.collision = "error"
hash.use_scope_name = true
hash.use_code_size = true
hash.use_item_names = false
hash.use_code_text = false
[package.metadata.sabry.lightningcss.targets]
chrome = "120"
safari = "13.2"
ie = "6"
```
### `sabry.css`
**bundle** *(no default)* - file path ro write CSS bundle into, relative to crate root
**prelude** *(no default)* - collection of CSS files, relative to the crate root, which content will be inserted before the compiled style into the *bundle* file if any. Does not affect generated CSS scopes if any.
**scopes** *(no default)* - dir path to put separate CSS for every scope into, relative to crate root
**minify** *(default true)* - print compressed CSS output and do the lightningcss thing
### `sabry.sass`
**intermediate_dir** *(default "target/.sabry/sass")* - file to put SASS/SCSS modules into so they are available with `@use` in code
**scanroot** *(default "src")* - root directory to start scanning "rs" files from. Used in build function
**modules** *(no default)* - collection of SASS/SCSS files, relative to the crate root, which should be available as modules as well
**prelude** *(no default)* - collection of SASS/SCSS files, relative to the crate root, which content will be compiled into CSS and then inserted into the CSS *bundle* if any. Does not affect generated CSS scopes if any.
**module_name_collision** *(default "merge")* - how to handle similary named modules.
*merge* - merge content
*error* - break building process with an error
### `sabry.hash`
**size** *(default 6)* - size of hash in bytes. Feel free to increase/decrease.
**use_scope_name** *(default true)* - wether to use scope identifier to calculate hash
**use_code_size** *(default true)* - wether to use scope code size to calculate hash
**use_item_names** *(default false)* - wether to use all scoped item idents to calculate hash
**use_code_text** *(default false)* - wether to use the scope code text to calculate hash
**collision** *(default "ignore")* - how to handle similarity of generated hashes
*ignore* - dont do anything
*error* - break building process with an error
### `sabry.lightningcss.targets`
Does require `css.minify` to be *true*.
Empty by default.
Available keys: chrome, firefox, edge, safari, saf_ios, samsung, android, ie
Value - minimal browser version to support in "M.m.p" format, where:
- *M* - major
- *m* - minor
- *p* - patch
For example `{ie = "9", saf_ios = "13.2"}` will try to generate CSS supported on both IE 9 and Safari-on-ios 13.2
## Detailed guide
### Style definition with `scssy!`
The `scssy!` macro is available with *procmacro* feature which is enabled by default.
It does accept the following syntax: `$name(:$syntax)? ({ $code })|($filename)`, where
- *$name* is any identifier valid for `macro_rules!`
- *$syntax* is either `sass` or `scss`
- *$code* is valid arbitrary style code in specified syntax
- *$filename* is a string literal which contains path to the file relative to package root
Examples:
```rust
use sabry::scssy;
scssy!(module1 {"$primary-color: red;"});
scssy!(module2 "tests/assets/mixin-module.scss");
scssy!(module3:sass "tests/assets/mixin-module.sass");
// works, but there are catches.
scssy!(module4:sass {"
@mixin colored($col: primary)
@if $col == primary
color: white
@else
color: red
"});
```
You may omit the syntax specifier - sabry uses SCSS as the default one.
The given code to `scssy!` is not checked to be valid code in given syntax (wip).
> SASS support inside of rust files is experimental. If you do want to use SASS tabbed syntax - consider to use files path instead of sass-in-rust option.
> With *nightly* feature flag if using the relative path like `scssy!(module "./module.scss")` you'll get false-positive error even if file exists. Also you won't get autocompletion and rust-analyzer will complain on `module!` macro. WIP.
### Scoping with `styly!`
The `styly!` macro is available with *procmacro* feature flag which is enabled by default.
It does accept the following syntax: `pub? const? $ident(:$syntax)? ({ $code })|($filename)`, where
- *pub* is explained [here](#public-styly-scopes)
- *const* is explained [here](#constant-styly-scopes)
- *$ident* is any identifier valid for `mod`
- *$syntax* is either `sass` or `scss`
- *$code* is arbitrary style code valid with given syntax
- *$filename* is a string literal which contains path to the file relative to package root
Examples
```rust
use sabry::styly;
styly!(private_fox {".fur {color: red; &-dark {color: black;}}"});
styly!(pub public_fox {".fur {color: red; &-dark {color: black;}}"});
styly!(pub const pub_compiletime_fox:sass {"
.fur
color: red
&-dark
color: black
"});
```
Every of those calls will produce the styling scope as a module. Differences are explained right below.
In general the scope does look like this:
```rust,ignore
const FOX: &str = "J9k_s9";
mod fox {
pub const fur: &str = "J9k_s9 fur";
pub fn _dark(c: &str) -> String {format!("{c}-dark")}
}
```
#### Styly scopes
Styly macro itself does not generate the scope. It is done in the `sabry_intrnl::scoper`. However, as a result, you will have the following:
- `const` with the UPPER_CASE name of the scope, which contains its hash
- `mod` with exact scope name which is the scope you're going to use
- for simple selectors, like `.fur` you will have a const members with hashed original selectors to use wherever you need a class/id/etc.
- for parent-selectors like `&-dark` you will have a non-const function, which will create matching the selector from any other.
You can read more about scoping and hashing in the [scoping](#scoping) section.
#### Public styly scopes
By default generated `mod` is private. You can make both mod and wrapper style constant public by adding the `pub` to macro call:
```rust
sabry::styly!(pub whatever "tests/assets/mixin-module.scss");
```
#### Constant styly scopes
As you've seen above, scope doe not contain any style code by itself. That's the use case i advise mostly.
However you could still compile styles into the artifact by simply adding the `const` to the macro call:
```rust
sabry::styly!(const scope "tests/assets/mixin-module.scss");
```
Which results in following:
```rust,ignore
const SCOPE: &str = /* scope hash */;
const SCOPE_CSS: &str = /* compiled from src/assets/scope.scss */;
mod scope {/* selector collection */}
```
> **There is a catch** | with the `const` modifier macro must compile CSS at compile-time. That results in several game changers:
>
> *First*. You could avoid the "build magic". Sabry will just compile given styles with procmacro at compile time.
>
> *Second*. If you `@use` something inside of constant-flavored scope, you can only success if sabry *did the build magic before compilation of that macro call*. So you still can compile the styles into the artifact and enjoy mixins from other crates, but, in general, you are going to receive some false-positives from editor.
>
> Worth of notice: sabry will still include const-flavored styles into the CSS bundle during build time.
> With *nightly* feature flag if using the relative path like `styly!(const scope "./sctyle.scss")` you'll get false-positive error even if file exists. Also you won't get autocompletion and rust-analyzer will complain on `SCOPE_CSS`, `scope::whatever` etc. WIP.
### Building with `buildy` and `usey!`
The `buildy` function is available with *build* feature which needs to be enabled explicitly.
This function accepts an iterator of pairs: (file_name, code) in form of `(String, String)` type. File name should have an extension, so grass can infere syntax during CSS compilation.
Each of those pairs is processed as a file which sabry needs to write into
the configured `intermediate_dir` and then passed into the CSS compiler.
You could, for example, define the module "mixin_a":
```rust,ignore
buildy(vec![("mixin_a".to_string(), "@mixin a(){}".to_string())]);
```
However there's a usey macro, which handles this for you:
```rust,ignore
buildy(
usey!(mixins!(), utils!())
);
```
The `usey!` macro accepts the following syntax:
`#($macro,)*`, where
- *$macro* is a macro which handles two expansions:
- `() => { $code }`
- `(syntax) => { $syntax }`
Where for the *$macro*:
- *$code* is a source code of style as a string literal
- *$name* is a name of module without extension as a string literal
- *$syntax* is either "sass" or "scss"
`usey!` macro will put the module full name from macro call identifier and the `syntax` expansion, so you could
resolve potential naming conflicts:
```rust,ignore
use tgk_brandstyle::theme;
use basestyle::theme as base_theme;
sabry::buildy(
sabry::usey!(
theme!(),
base_theme!()
)
)?;
```
Exactly this kind of macros is produced by [`scssy!`](#style-definition-with-scssy).
### Scoping
Sabry handles scoping by restriction or mutation of existing selectors
with the hash. Hash is calculated for the entire scope by the [`styly!`](#scoping-with-styly) macro.
Currently the following types of selectors are scoped:
- class
- id
- SASS parent selectors (see below, there is a catch)
- tagname (see below, there is a catch)
Sabry does not make difference between top-level and nested selectors,
also selector complexity isn't taken into account: sabry simply walks through all compound selectors
and apply scoping for supported ones.
Different selector types are scoped differently:
- class selectors are restricted with scope hash: `.class` -> `.HASH.class`
- id selector are mutated with scope hash: `#id` -> `#id-HASH`
- tagname selectors are changed into indirect descendance: `div` -> `.HASH div`
> **There is a catch** | currently tagname selectors are scoped with indirect descendancy
> hash restriction. That means - if you do use tagnames in scoped stylesheets, you have to wrap your markdown
> with `SCOPE` class generated by `styly!` macro.
>
> 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:
```rust
sabry::styly!(scope {"
.cls1 {
&-dark {}
}
"});
```
is something like
```rust,ignore
const SCOPE: &str = "Ut8CskJ";
mod scope {
pub const cls1: &str = "Ut8CskJ cls1";
pub fn _dark(c: &str) -> String {format!("{c}-dark")}
}
```
So you still can get the scoped variant of `cls1-dark` class: `scope::_dark(scope::cls1)`.
This isn't very handy, also isn't strict enough, and is a high-priority subject to change. wip.
#### Scope member naming rules
Not any valid CSS selector is a valid rust identifier. In general this section should not be needed, as you should receive autocompletion from the editor. *However* it doesn't seem to work properly. Check the [wip](#wip) section out.
## Notable feature flags
**build** - turns on the `sabry::buildy` function, along with the entire `sabry_build` crate where it lives
**internals** - exposes majority of internal stuff for you to experiment or build own workflow
**lepty-scoping** - overhauls the scope generation logic, best suitable for the leptos. Check out the [section](#leptos-specials) and an [example](https://github.com/yiffyrusdev/sabry/tree/master/examples/leptos-axum)
**nightly** - allows relative path selection with `scssy!` and `styly!` macros. However rust-analyzer will raise false-positives for reachable files as well.
## WIP
*(sorted by my own priority)*, "dones" are excluded
- [x] Somehow achieve the autocompletion for scopes. The problem is explained in details [here](https://github.com/yiffyrusdev/sabry/issues/2)
- [x] some weird unrelated stuff I can see in autocompletion (still investigating btw)
- [ ] Support for direct CSS syntax
- [ ] Currently the crate causes "dependency inheritance" infection. We cant get rid of it, however should be doable to at least get rid of flag inheritance
- [ ] Experience with cargo-leptos is fine, and we do use it, however its a bit "raughy". Need to do something about it:
- [ ] We have to save file twice with cargo leptos for changed styles to take effect
- [ ] Seems like turning on the "hashing" with leptos requires a hard-restart of cargo-leptos
## Contributions
Any contributions are always welcome!
If you find this crate usefull, wanna stick with it in some project, but do miss some
features - feel free to submit a PR.
> If you'd like to fork for the PR - please, use the "window" branch, not the "master".
If you encounter any bugs/problems or have use case where things dont work as they should
for the latest version - please, open an issue!
If you'd like to lend a paw - feel free to check the [WIP](#wip) section out, or to search for "TODO" comments.
## MSRV
Sabry passes its own tests on 1.78 nightly/stable.
Examples are on 1.82.
## License
MIT.