sabry 0.0.6

Syntactically Awesome But RustY - crate that brings SCSS/SASS into rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# 🧙🏻 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
>

[![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:

<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/>

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;}"});
```
```html
/// something like render function
{
    <div class={concatcp!("meow ", CATS)}>
        <ul class={concatcp!("howl ", DOGS)}></ul>
    </div>
}
```
```html
/// or even better with leptos
view!{class=CATS
    <div class="meow">
        {move || view!{class=DOGS,
            <ul class="howl"></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;}"});
```

```rust,ignore
// requires `const-scoping` feature
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:

|[crate of styles]https://github.com/yiffyrusdev/sabry/tree/master/examples/define-styles|[style usage]https://github.com/yiffyrusdev/sabry/tree/master/examples/use-styles|[leptos-axum with sabry]https://github.com/yiffyrusdev/sabry/tree/master/examples/leptos-axum|[leptos components]https://github.com/yiffyrusdev/sabry/tree/master/examples/leptos-components|
|-|-|-|-|

### Create a crate full of arbitrary SASS

The only need is the dependency

```toml
#Cargo.toml

[dependencies]
sabry = {version = "0.0.6"}
```
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.6"}
```
And create a style scope wherever you want:
```rust
// breadbadgelist.rs
use sabry::styly;

styly!(styles {"
    .badges {
        display: flex;
        &__list {
            display: flex;
        }
    }
    #wolf {
        color: white;
    }
"});
```

```html
<ul class={concatcp!("badges ", STYLES)}>
    <li class={STYLES} id="wolf">
            "HOOOOWL!"
    </li>
</ul>
```

### Use styles earlier created in another crate

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.6"}

[build-dependencies]
sabry = {version = "0.0.6", 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;

// required `const-scoping` feature
styly!(pub const scope:scss {"
    .whatever {
        &__code {}
        &[you-please] {}
    }
"});

// requires `const-scoping` feature
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;

// requires `const-scoping` feature
styly!(pub const scope:scss {"
    .whatever {
        &__code {}
        &[you-please] {}
    }
"});

// requires `const-scoping` feature
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>

## 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;}}"});
```
```rust,ignore
// requires `const-scoping` feature
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 {
    #[deprecated(since = "0.0.6", note = "will remove")]
    pub const fur: &str = "fur J9k_s9";
}
```

#### 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

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,ignore
// Requires `const-scoping` feature
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 */;
```

> **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 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
- tagname
- SASS parent selectors (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` -> `.HASH#id`
- tagname selectors are changed into indirect descendance: `div` -> `div.HASH`

#### 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)

**const-scoping** - unlocks `styly!(const scope...` compile-time SASS->CSS generation, which seems to break WASM builds without special treatment (https://docs.rs/getrandom/latest/getrandom/#webassembly-support). Idk why, but it started to happen just recently

**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

- [ ] Why does `const scope` break WASM build? Everything was fine untill recent. This feature isn't top-priority, I dont personally use it, but still curious
- [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.88 nightly/stable.

Examples are on 1.88.

## License

MIT.