sasso 0.2.7 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -2
- data/Cargo.lock +3 -3
- data/README.md +79 -7
- data/ext/sasso/Cargo.toml +3 -3
- data/ext/sasso/src/lib.rs +175 -76
- data/lib/sasso/version.rb +6 -3
- data/lib/sasso.rb +66 -6
- data/sig/sasso.rbs +29 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48e0d49603b5fa0b739a8f52c44b52b6dffdf7ead5561fab036584829f0c5b57
|
|
4
|
+
data.tar.gz: 5dc5d4d1d8dab9d7202465f73a989e9e4cb6aed5b25811d5d090b3e100edf575
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a5fc9a1351d5b31b705c9763ca0c5bf4981d5c56782f949099a1228e14be24fc8aef56866c3cad7624655883dda2383eb792376b864aab3d359cd56eba5cc47
|
|
7
|
+
data.tar.gz: f6b9ae0fd2f91181439c7bfda2002aeb4845a6219ab41055cffe8a5ac41a02d54d3ed30d0fad54fe2e6311b55c7ab77ff4fd0151b3a1f41a0e13f90a498375e0
|
data/CHANGELOG.md
CHANGED
|
@@ -3,11 +3,112 @@
|
|
|
3
3
|
All notable changes to the **sasso** Ruby gem are documented here.
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
notes the exact core crate version it pins.
|
|
6
|
+
Since 0.14.0 the gem version tracks the `sasso` compiler crate it bundles; each
|
|
7
|
+
release notes the exact core crate version it pins. Releases up to 0.2.7 versioned
|
|
8
|
+
the gem independently of the crate.
|
|
8
9
|
|
|
9
10
|
## [Unreleased]
|
|
10
11
|
|
|
12
|
+
## [0.14.0] - 2026-09-17
|
|
13
|
+
|
|
14
|
+
_The gem version now tracks the core compiler's. It jumps 0.2.7 → 0.14.0 to meet
|
|
15
|
+
`sasso` 0.14.0, adopting seven core releases at once (0.7.0 through 0.14.0)._
|
|
16
|
+
|
|
17
|
+
_**Why align.** The gem version floated independently, so "sasso 0.2.7" said
|
|
18
|
+
nothing about which compiler it carried: the three framework gems each had to
|
|
19
|
+
document the mapping, and a bug report needed both numbers to be actionable.
|
|
20
|
+
From here, a gem release adopting core X.Y.Z **is** gem X.Y.Z. A gem-only fix
|
|
21
|
+
takes the next patch, so the gem may sit ahead of the crate within a minor —
|
|
22
|
+
the new `Sasso::CORE_VERSION` always reports what is actually linked._
|
|
23
|
+
|
|
24
|
+
_The Ruby API is backward compatible; the CSS the compiler emits is not, in the
|
|
25
|
+
ways listed below._
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
- **Ruby 3.1 support** (`required_ruby_version` is now `>= 3.2.0`). It left
|
|
30
|
+
security maintenance in March 2025, and is off the precompiled-gem matrix.
|
|
31
|
+
|
|
32
|
+
### Changed (output — dart-sass 1.101.4 → 1.104.1 alignment)
|
|
33
|
+
|
|
34
|
+
The gem's Ruby API is unchanged, but the CSS it emits moved with the core. If
|
|
35
|
+
you byte-compare output — snapshot tests, asset digests, build caches — expect
|
|
36
|
+
diffs. The CSS is equivalent; only its spelling changed.
|
|
37
|
+
|
|
38
|
+
- **Global `whiteness()` / `blackness()` are no longer built-ins** (core 0.9.0):
|
|
39
|
+
they are `sass:color`-only, so the bare call is now an unknown function and
|
|
40
|
+
passes through as plain CSS instead of being evaluated. `whiteness(#f00)` goes
|
|
41
|
+
from `0%` to `whiteness(#f00)`, and `1 + whiteness(#f00)` to `1whiteness(#f00)`
|
|
42
|
+
— both byte-identical to dart-sass 1.104.1, and both **silent**: no error, no
|
|
43
|
+
warning. This is the change in this release to grep your stylesheets for. Use
|
|
44
|
+
`color.whiteness()` / `color.blackness()` via `@use "sass:color"`.
|
|
45
|
+
- A legacy color with any fractional channel writes its rgb triple as
|
|
46
|
+
**percentages**: `rgb(127.5, 0, 127.5)` becomes `rgb(50%, 0%, 50%)` (0.9.0).
|
|
47
|
+
- **Compressed hsl/hwb route through rgb** like every other legacy space, so
|
|
48
|
+
`darken(#336699, 10%)` compresses to `rgb(15%,30%,45%)` rather than
|
|
49
|
+
`hsl(210,50%,30%)` (0.9.0).
|
|
50
|
+
- **A negative zero keeps its sign**: `0 * -1`, `-0` and `math.div(0, -1)`
|
|
51
|
+
serialize as `-0`. The sign is the IEEE sign bit, so `0 - 0` stays `0` (0.10.0).
|
|
52
|
+
- **Colors convert their degenerate channels**: a `NaN` channel becomes `0`, and
|
|
53
|
+
a polar hue converts every non-finite value (0.10.0).
|
|
54
|
+
- **`rec2020` uses the pure 2.4 gamma transfer function**, replacing the BT.2020
|
|
55
|
+
piecewise curve (0.9.0).
|
|
56
|
+
- **Plain-CSS `if()` emits in CSS serialization format**, not `meta.inspect`
|
|
57
|
+
format: lists lose their parens, `null` serializes to nothing (0.9.0).
|
|
58
|
+
- **A comment before `@use` is emitted exactly once**; a repeat edge into an
|
|
59
|
+
already-loaded module no longer re-emits it (0.10.0).
|
|
60
|
+
- Extensive **`@extend`, module-system and selector line-break fidelity** fixes
|
|
61
|
+
(0.7.0, 0.8.0) — the work that took all 20 projects in the core's real-world
|
|
62
|
+
corpus to byte-identical with dart-sass.
|
|
63
|
+
|
|
64
|
+
### Changed (source maps)
|
|
65
|
+
|
|
66
|
+
- **A declaration whose value is a bare `$name` maps back to the variable's
|
|
67
|
+
definition**, transitively through `$b: $a` chains, module members and
|
|
68
|
+
mixin/function parameters (0.9.0). The segment used to be omitted, which also
|
|
69
|
+
renumbered every following delta-encoded segment — so a recorded `mappings`
|
|
70
|
+
string changes.
|
|
71
|
+
|
|
72
|
+
### Changed (diagnostics)
|
|
73
|
+
|
|
74
|
+
- **Function arity errors follow dart's wording**: only positional arguments
|
|
75
|
+
count, and the word "positional" appears once any named argument is in play
|
|
76
|
+
(0.10.0).
|
|
77
|
+
- **An error inside a loaded file is attributed to that file**, with one stack
|
|
78
|
+
frame per loader, for `@use`, `@forward` and `@import` chains alike (0.7.0).
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- **`charset:`** (dart-sass `charset`) — `false` omits the `@charset "UTF-8";`
|
|
83
|
+
prefix, or the U+FEFF BOM when compressed, that non-ASCII output carries.
|
|
84
|
+
- **`quiet:`** — print no `@warn`/`@debug`/deprecation diagnostics. They have
|
|
85
|
+
always gone to `$stderr`; there was previously no way off.
|
|
86
|
+
- **`quiet_deps:`** (dart-sass `quietDeps`) — drop deprecation warnings raised
|
|
87
|
+
inside dependencies (files resolved through a load path), while the entry
|
|
88
|
+
stylesheet's own still print. `@warn` is untouched either way.
|
|
89
|
+
- **`on_warn:`** — a callable receiving each diagnostic as a Hash of the new
|
|
90
|
+
`Sasso::WARNING_KEYS`, which replaces the stderr printing rather than
|
|
91
|
+
duplicating it. `:formatted` carries the full dart-style block, so the
|
|
92
|
+
compiler's own rendering can go straight into an application logger.
|
|
93
|
+
Mutually exclusive with `quiet:`. A compile that warns and then fails delivers
|
|
94
|
+
its warnings before raising `Sasso::CompileError`, the way dart-sass hands them
|
|
95
|
+
to its logger before throwing.
|
|
96
|
+
- **`Sasso::CORE_VERSION`** — the bundled compiler crate's version, read from
|
|
97
|
+
the linked binary so it cannot drift from what is loaded.
|
|
98
|
+
|
|
99
|
+
### Documentation
|
|
100
|
+
|
|
101
|
+
- The README documents **`source_map:` and `CompileResult` for the first time**;
|
|
102
|
+
they shipped in gem 0.2.0 and never reached it. `sig/sasso.rbs` gains them too,
|
|
103
|
+
alongside the new options.
|
|
104
|
+
- The **Performance table is remeasured**: this working tree (core 0.14.0)
|
|
105
|
+
against `sass-embedded` 1.104.1 and `sassc` 2.4.0. The ~180-rule case is ~10%
|
|
106
|
+
faster. The cold-start row is **corrected upward**, from a claimed 1.1 ms to a
|
|
107
|
+
measured 3.2 ms — that figure reproduced on neither the old gem nor the new, so
|
|
108
|
+
it predates this release; the margin over `sass-embedded` is 12.7×, not 35×.
|
|
109
|
+
- `benchmark/Gemfile` takes **`SASSO_PATH`** to benchmark a working tree instead
|
|
110
|
+
of the published gem, which is what the README's table now reports.
|
|
111
|
+
|
|
11
112
|
## [0.2.7] - 2026-06-25
|
|
12
113
|
|
|
13
114
|
### Changed
|
data/Cargo.lock
CHANGED
|
@@ -239,14 +239,14 @@ dependencies = [
|
|
|
239
239
|
"magnus",
|
|
240
240
|
"rb-sys",
|
|
241
241
|
"rb-sys-env",
|
|
242
|
-
"sasso 0.
|
|
242
|
+
"sasso 0.14.0",
|
|
243
243
|
]
|
|
244
244
|
|
|
245
245
|
[[package]]
|
|
246
246
|
name = "sasso"
|
|
247
|
-
version = "0.
|
|
247
|
+
version = "0.14.0"
|
|
248
248
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
249
|
-
checksum = "
|
|
249
|
+
checksum = "f6ae98548b2e9447aca2340b1c082c82d06721586708b808c52e2f908499c2f8"
|
|
250
250
|
|
|
251
251
|
[[package]]
|
|
252
252
|
name = "seq-macro"
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ In-process **SCSS / Sass → CSS** compilation for Ruby, backed by [**sasso**](h
|
|
|
6
6
|
|
|
7
7
|
> This gem is the Ruby binding. The compiler core lives in the separate
|
|
8
8
|
> [`momiji-rs/sasso`](https://github.com/momiji-rs/sasso) repo (crate on
|
|
9
|
-
> crates.io); this gem pins it exactly and
|
|
9
|
+
> crates.io); this gem pins it exactly and carries its version number.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -52,6 +52,78 @@ Sasso.compile("app/assets/stylesheets/application.scss",
|
|
|
52
52
|
| `load_paths:` | `[]` | directories searched for `@use`/`@forward`/`@import` |
|
|
53
53
|
| `url:` | `nil` | filename shown in diagnostics (enables the rich dart-style error block) |
|
|
54
54
|
| `alert_ascii:` | `false` | ASCII-only diagnostics |
|
|
55
|
+
| `charset:` | `true` | prefix non-ASCII output with `@charset "UTF-8";` (a BOM when compressed) |
|
|
56
|
+
| `source_map:` | `false` | return a `Sasso::CompileResult` instead of a String |
|
|
57
|
+
| `source_map_include_sources:` | `false` | embed each source's text in the map's `sourcesContent` |
|
|
58
|
+
| `quiet:` | `false` | print no `@warn`/`@debug`/deprecation diagnostics |
|
|
59
|
+
| `quiet_deps:` | `false` | drop deprecation warnings raised inside dependencies |
|
|
60
|
+
| `on_warn:` | `nil` | a callable receiving each diagnostic; replaces the stderr printing |
|
|
61
|
+
|
|
62
|
+
### Source maps
|
|
63
|
+
|
|
64
|
+
With `source_map: true` the return value is a `Sasso::CompileResult` — `#css` is
|
|
65
|
+
the same String you would get otherwise, and `#source_map` is a parsed Source Map
|
|
66
|
+
v3 Hash. Pass `url:` so the map can name the entry stylesheet.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
r = Sasso.compile_string(scss, source_map: true, url: "application.scss")
|
|
70
|
+
r.css # => "a {\n color: red;\n}"
|
|
71
|
+
r.source_map["version"] # => 3
|
|
72
|
+
r.source_map["sources"] # => ["application.scss"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Diagnostics
|
|
76
|
+
|
|
77
|
+
`@warn`, `@debug` and deprecation warnings print to `$stderr` by default, the
|
|
78
|
+
same dart-style block the `sasso` CLI and dart-sass print. Two ways to change
|
|
79
|
+
that:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# Silence them entirely:
|
|
83
|
+
Sasso.compile_string(scss, quiet: true)
|
|
84
|
+
|
|
85
|
+
# Silence only what dependencies deprecate (dart-sass `quietDeps`) — files
|
|
86
|
+
# resolved through a load path. The entry stylesheet's own still print.
|
|
87
|
+
Sasso.compile_string(scss, load_paths: ["vendor/stylesheets"], quiet_deps: true)
|
|
88
|
+
|
|
89
|
+
# Or take delivery yourself, which suppresses the printing:
|
|
90
|
+
Sasso.compile_string(scss, url: "in.scss", on_warn: ->(d) {
|
|
91
|
+
next if d[:deprecation_id] == "color-functions"
|
|
92
|
+
|
|
93
|
+
Rails.logger.warn(d[:formatted]) # the block the compiler would have printed
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each diagnostic is a Hash of `Sasso::WARNING_KEYS`: `:kind` (`:warn`/`:debug`),
|
|
98
|
+
`:deprecation`, `:deprecation_id`, `:message`, `:formatted`, `:url`, `:line` and
|
|
99
|
+
`:path`. `:url` is dart's display form of the file; `:path` identifies it (the
|
|
100
|
+
importer's canonical path), which is what distinguishes a dependency from the
|
|
101
|
+
entry stylesheet. `quiet:` and `on_warn:` are mutually exclusive.
|
|
102
|
+
|
|
103
|
+
Two things to know about what `on_warn:` receives:
|
|
104
|
+
|
|
105
|
+
- A compile that warns and then **fails** delivers its warnings first and raises
|
|
106
|
+
`Sasso::CompileError` after, so a diagnostic is never lost to the error — the
|
|
107
|
+
callable is the only thing reporting them once it has taken over. If the
|
|
108
|
+
callable itself raises, the `CompileError` still wins and carries that
|
|
109
|
+
exception as its `#cause`.
|
|
110
|
+
- Like dart-sass, the compiler **caps a repeated deprecation at five per id** and
|
|
111
|
+
then emits one summary diagnostic — `"N repetitive deprecation warnings
|
|
112
|
+
omitted."` — with no `:deprecation_id`, no `:url` and `:line` of `0`. Code that
|
|
113
|
+
keys on the id or formats `"#{d[:url]}:#{d[:line]}"` should expect it. There is
|
|
114
|
+
no verbose mode that lifts the cap; the compiler core does not offer one.
|
|
115
|
+
|
|
116
|
+
### Versions
|
|
117
|
+
|
|
118
|
+
Since 0.14.0 the gem version tracks the core compiler crate it bundles: gem
|
|
119
|
+
0.14.0 pins crate 0.14.0. A gem-only fix takes the next patch, so `Sasso::VERSION`
|
|
120
|
+
may sit ahead of the crate within a minor — `Sasso::CORE_VERSION` reads the
|
|
121
|
+
version out of the linked binary and is the authority on what is loaded.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Sasso::VERSION # => "0.14.0" the gem
|
|
125
|
+
Sasso::CORE_VERSION # => "0.14.0" the compiler actually linked in
|
|
126
|
+
```
|
|
55
127
|
|
|
56
128
|
### Errors
|
|
57
129
|
|
|
@@ -92,20 +164,20 @@ IPC, no Dart VM), it avoids the per-call protocol roundtrip of `sass-embedded`
|
|
|
92
164
|
and the process-spawn cost paid by any out-of-process compiler.
|
|
93
165
|
|
|
94
166
|
Compiling the same SCSS (variables, nesting, `@for`, math) on an Apple M2 Max,
|
|
95
|
-
Ruby 3.4.1:
|
|
167
|
+
Ruby 3.4.1, against `sass-embedded` 1.104.1 and `sassc` 2.4.0:
|
|
96
168
|
|
|
97
169
|
| | `sasso` (this gem) | `sass-embedded` (dart-sass) | `sassc` (libsass) |
|
|
98
170
|
| --- | --: | --: | --: |
|
|
99
|
-
| Warm — small component (256 B) | **13.
|
|
100
|
-
| Warm — ~180 rules (5.5 KB) | **
|
|
101
|
-
| Cold start (`require` + first compile) | **
|
|
171
|
+
| Warm — small component (256 B) | **13.3 µs** | 125 µs (**9.4×**) | 1148 µs (87×) |
|
|
172
|
+
| Warm — ~180 rules (5.5 KB) | **215 µs** | 906 µs (**4.2×**) | 10234 µs (48×) |
|
|
173
|
+
| Cold start (`require` + first compile) | **3.2 ms** | 40.6 ms (**12.7×**) | 37.0 ms (12×) |
|
|
102
174
|
|
|
103
175
|
Parenthesised values are how much slower the other gem is than `sasso`.
|
|
104
176
|
|
|
105
177
|
- **Per-request compiling** (e.g. a Sinatra route): in-process latency is ~13 µs
|
|
106
|
-
vs ~
|
|
178
|
+
vs ~125 µs for `sass-embedded`'s pipe roundtrip to its Dart subprocess.
|
|
107
179
|
- **One-shot builds** (e.g. `rails assets:precompile`): the dominant cost is the
|
|
108
|
-
~
|
|
180
|
+
~41 ms Dart subprocess spawn, which `sasso` does not pay (~3 ms cold).
|
|
109
181
|
|
|
110
182
|
The engine is also heavily perf-tuned (a scoped bump arena, reference-counted
|
|
111
183
|
values). Numbers are representative of one machine; run your own with your
|
data/ext/sasso/Cargo.toml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The Ruby native-extension wrapper: a cdylib around `sasso::compile`, shipped
|
|
2
2
|
# inside the `sasso` gem (NOT published to crates.io). Depends on the PUBLISHED
|
|
3
|
-
# core crate via an exact pin — bump it deliberately to adopt a new core release
|
|
4
|
-
#
|
|
3
|
+
# core crate via an exact pin — bump it deliberately to adopt a new core release,
|
|
4
|
+
# and move the gem version to match (see lib/sasso/version.rb).
|
|
5
5
|
[package]
|
|
6
6
|
# Must equal the rake-compiler ExtensionTask name (rb_sys looks the package up
|
|
7
7
|
# by this name). The core compiler is depended on under a RENAMED key
|
|
@@ -29,7 +29,7 @@ rb-sys = "0.9"
|
|
|
29
29
|
# The published core compiler, pinned exactly to the API surface this binding
|
|
30
30
|
# targets. Renamed to `sasso_core` so this package can be named `sasso` (a
|
|
31
31
|
# package may not depend on a crate of its own name without a rename).
|
|
32
|
-
sasso_core = { package = "sasso", version = "=0.
|
|
32
|
+
sasso_core = { package = "sasso", version = "=0.14.0" }
|
|
33
33
|
|
|
34
34
|
[build-dependencies]
|
|
35
35
|
rb-sys-env = "0.1"
|
data/ext/sasso/src/lib.rs
CHANGED
|
@@ -1,82 +1,117 @@
|
|
|
1
1
|
//! Magnus binding around `sasso::compile`. Exposes a flat native ABI
|
|
2
|
-
//! `Sasso::Native._compile(
|
|
2
|
+
//! `Sasso::Native._compile(source, opts)`; the ergonomic keyword API lives in
|
|
3
3
|
//! `lib/sasso.rb`. The core crate is `unsafe`-free; this thin FFI layer is the
|
|
4
4
|
//! `unsafe` boundary (magnus hides it — we write no explicit `unsafe`).
|
|
5
5
|
//!
|
|
6
|
+
//! Options travel in ONE hash rather than as positional arguments, so adopting
|
|
7
|
+
//! a new compiler option costs a key here instead of an ABI arity change on
|
|
8
|
+
//! both sides. `Sasso::Native` is a private ABI: `lib/sasso.rb` is its only
|
|
9
|
+
//! caller and validates every value before it arrives.
|
|
10
|
+
//!
|
|
6
11
|
//! Importer policy (v1): a built-in Rust `FsImporter` driven by `load_paths`.
|
|
7
12
|
//! A Ruby-callback importer is deferred (GC-pinning + GVL re-entrancy hazards).
|
|
8
13
|
|
|
9
|
-
use magnus::{function, prelude::*,
|
|
14
|
+
use magnus::{function, prelude::*, value::ReprValue, Error, RArray, RHash, Ruby, TryConvert};
|
|
10
15
|
use sasso_core as sasso; // the core crate, renamed in Cargo.toml to free the `sasso` package name
|
|
16
|
+
use std::cell::RefCell;
|
|
11
17
|
use std::path::PathBuf;
|
|
18
|
+
use std::rc::Rc;
|
|
12
19
|
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
fn
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.unwrap_or_else(|_| ruby.exception_runtime_error());
|
|
21
|
-
Error::new(klass, msg)
|
|
20
|
+
/// Read one option out of the hash. A missing key and an explicit `nil` read
|
|
21
|
+
/// the same, so the Ruby wrapper can pass a key through unconditionally.
|
|
22
|
+
fn opt<T: TryConvert>(ruby: &Ruby, opts: RHash, key: &str) -> Result<Option<T>, Error> {
|
|
23
|
+
match opts.get(ruby.sym_new(key)) {
|
|
24
|
+
Some(v) if !v.is_nil() => Ok(Some(T::try_convert(v)?)),
|
|
25
|
+
_ => Ok(None),
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
///
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
source: String,
|
|
29
|
-
style: String,
|
|
30
|
-
syntax: String,
|
|
31
|
-
load_paths: RArray,
|
|
32
|
-
url: Option<String>,
|
|
33
|
-
unicode: bool,
|
|
34
|
-
) -> Result<String, Error> {
|
|
35
|
-
let mut opts = sasso::Options::default()
|
|
36
|
-
.with_style(if style == "compressed" {
|
|
37
|
-
sasso::OutputStyle::Compressed
|
|
38
|
-
} else {
|
|
39
|
-
sasso::OutputStyle::Expanded
|
|
40
|
-
})
|
|
41
|
-
.with_syntax(match syntax.as_str() {
|
|
42
|
-
"sass" => sasso::Syntax::Sass,
|
|
43
|
-
"css" => sasso::Syntax::Css,
|
|
44
|
-
_ => sasso::Syntax::Scss,
|
|
45
|
-
})
|
|
46
|
-
.with_unicode(unicode);
|
|
29
|
+
/// Read a boolean option, falling back to the core crate's own default.
|
|
30
|
+
fn flag(ruby: &Ruby, opts: RHash, key: &str, default: bool) -> Result<bool, Error> {
|
|
31
|
+
Ok(opt::<bool>(ruby, opts, key)?.unwrap_or(default))
|
|
32
|
+
}
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
/// Read `load_paths` as filesystem paths.
|
|
35
|
+
fn load_paths(ruby: &Ruby, opts: RHash) -> Result<Vec<PathBuf>, Error> {
|
|
36
|
+
match opt::<RArray>(ruby, opts, "load_paths")? {
|
|
37
|
+
Some(a) => Ok(a
|
|
38
|
+
.to_vec::<String>()?
|
|
39
|
+
.into_iter()
|
|
40
|
+
.map(PathBuf::from)
|
|
41
|
+
.collect()),
|
|
42
|
+
None => Ok(Vec::new()),
|
|
51
43
|
}
|
|
44
|
+
}
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
/// One `@warn` / `@debug` / deprecation diagnostic, copied out of the borrowed
|
|
47
|
+
/// `WarnEvent` so it outlives the compile that produced it.
|
|
48
|
+
///
|
|
49
|
+
/// The handler runs DURING the compile, and calling back into Ruby from there
|
|
50
|
+
/// would re-enter the VM mid-compile; instead every event is recorded here and
|
|
51
|
+
/// handed to Ruby once `compile` has returned. That trades memory for safety: a
|
|
52
|
+
/// compile raising N warnings holds N of these until it finishes, where the
|
|
53
|
+
/// default stderr path streams them out and retains nothing. The core caps
|
|
54
|
+
/// repeated deprecations at five per id, so in practice only `@warn`/`@debug` in
|
|
55
|
+
/// a loop can grow the buffer without bound.
|
|
56
|
+
struct Warning {
|
|
57
|
+
kind: &'static str,
|
|
58
|
+
deprecation: bool,
|
|
59
|
+
deprecation_id: String,
|
|
60
|
+
message: String,
|
|
61
|
+
formatted: String,
|
|
62
|
+
url: String,
|
|
63
|
+
line: usize,
|
|
64
|
+
path: String,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl Warning {
|
|
68
|
+
fn record(event: &sasso::WarnEvent<'_>) -> Self {
|
|
69
|
+
Warning {
|
|
70
|
+
kind: match event.kind {
|
|
71
|
+
sasso::WarnKind::Debug => "debug",
|
|
72
|
+
sasso::WarnKind::Warn => "warn",
|
|
73
|
+
},
|
|
74
|
+
deprecation: event.deprecation,
|
|
75
|
+
deprecation_id: event.deprecation_id.to_owned(),
|
|
76
|
+
message: event.message.to_owned(),
|
|
77
|
+
formatted: event.formatted.to_owned(),
|
|
78
|
+
url: event.url.to_owned(),
|
|
79
|
+
line: event.line,
|
|
80
|
+
path: event.path.to_owned(),
|
|
81
|
+
}
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
84
|
+
fn into_hash(self, ruby: &Ruby) -> Result<RHash, Error> {
|
|
85
|
+
let h = ruby.hash_new();
|
|
86
|
+
h.aset(ruby.sym_new("kind"), ruby.sym_new(self.kind))?;
|
|
87
|
+
h.aset(ruby.sym_new("deprecation"), self.deprecation)?;
|
|
88
|
+
h.aset(ruby.sym_new("deprecation_id"), self.deprecation_id)?;
|
|
89
|
+
h.aset(ruby.sym_new("message"), self.message)?;
|
|
90
|
+
h.aset(ruby.sym_new("formatted"), self.formatted)?;
|
|
91
|
+
h.aset(ruby.sym_new("url"), self.url)?;
|
|
92
|
+
h.aset(ruby.sym_new("line"), self.line)?;
|
|
93
|
+
h.aset(ruby.sym_new("path"), self.path)?;
|
|
94
|
+
Ok(h)
|
|
95
|
+
}
|
|
65
96
|
}
|
|
66
97
|
|
|
67
|
-
///
|
|
68
|
-
///
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let
|
|
98
|
+
/// Flat native ABI:
|
|
99
|
+
/// `_compile(source, opts) -> [css, source_map_json, warnings, error]`.
|
|
100
|
+
///
|
|
101
|
+
/// A compile failure RETURNS its message in `error` rather than raising here,
|
|
102
|
+
/// so the recorded warnings come back with it: `lib/sasso.rb` delivers them to
|
|
103
|
+
/// `on_warn:` and then raises `Sasso::CompileError`. dart-sass's logger sees the
|
|
104
|
+
/// warnings a failing compile raised too, and swallowing them would be worse
|
|
105
|
+
/// than not forwarding them — under `on_warn:` nothing else prints them.
|
|
106
|
+
/// Never panics across FFI — every other failure is a raised Ruby exception.
|
|
107
|
+
fn native_compile(ruby: &Ruby, source: String, opts: RHash) -> Result<RArray, Error> {
|
|
108
|
+
let style = opt::<String>(ruby, opts, "style")?.unwrap_or_default();
|
|
109
|
+
let syntax = opt::<String>(ruby, opts, "syntax")?.unwrap_or_default();
|
|
110
|
+
let url = opt::<String>(ruby, opts, "url")?;
|
|
111
|
+
let paths = load_paths(ruby, opts)?;
|
|
112
|
+
let want_map = flag(ruby, opts, "source_map", false)?;
|
|
113
|
+
|
|
114
|
+
let mut copts = sasso::Options::default()
|
|
80
115
|
.with_style(if style == "compressed" {
|
|
81
116
|
sasso::OutputStyle::Compressed
|
|
82
117
|
} else {
|
|
@@ -87,33 +122,97 @@ fn native_compile_with_map(
|
|
|
87
122
|
"css" => sasso::Syntax::Css,
|
|
88
123
|
_ => sasso::Syntax::Scss,
|
|
89
124
|
})
|
|
90
|
-
.with_unicode(unicode)
|
|
91
|
-
.with_source_map_include_sources(
|
|
125
|
+
.with_unicode(flag(ruby, opts, "unicode", true)?)
|
|
126
|
+
.with_source_map_include_sources(flag(ruby, opts, "source_map_include_sources", false)?)
|
|
127
|
+
.with_charset(flag(ruby, opts, "charset", true)?);
|
|
92
128
|
|
|
129
|
+
// `url` is load-bearing: it ENABLES the byte-exact dart diagnostic block.
|
|
93
130
|
if let Some(ref u) = url {
|
|
94
|
-
|
|
131
|
+
copts = copts.with_url(u);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Bind the importer for the whole `compile` call (Options borrows it).
|
|
135
|
+
let has_paths = !paths.is_empty();
|
|
136
|
+
let importer = sasso::FsImporter::new(paths);
|
|
137
|
+
if has_paths {
|
|
138
|
+
copts = copts.with_importer(&importer);
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
let importer = sasso::FsImporter::new(paths.clone());
|
|
103
|
-
if !paths.is_empty() {
|
|
104
|
-
opts = opts.with_importer(&importer);
|
|
141
|
+
// dart-sass `quietDeps`: classified by how a file was RESOLVED, so the set
|
|
142
|
+
// has to come from the importer that resolved it. Harmless with no load
|
|
143
|
+
// paths — nothing can be a dependency, so the set stays empty.
|
|
144
|
+
if flag(ruby, opts, "quiet_deps", false)? {
|
|
145
|
+
copts = copts.with_quiet_deps(importer.dependencies());
|
|
105
146
|
}
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
148
|
+
// "stderr" leaves `Options::warn` unset, which is what makes the core print
|
|
149
|
+
// its own dart-style block — the default path installs no handler and pays
|
|
150
|
+
// nothing, not even the sink's allocation. Only "capture" allocates.
|
|
151
|
+
let mut captured: Option<Rc<RefCell<Vec<Warning>>>> = None;
|
|
152
|
+
match opt::<String>(ruby, opts, "warnings")?
|
|
153
|
+
.unwrap_or_default()
|
|
154
|
+
.as_str()
|
|
155
|
+
{
|
|
156
|
+
"silence" => copts = copts.with_warn_handler(Rc::new(|_| {})),
|
|
157
|
+
"capture" => {
|
|
158
|
+
let sink: Rc<RefCell<Vec<Warning>>> = Rc::new(RefCell::new(Vec::new()));
|
|
159
|
+
let handler = Rc::clone(&sink);
|
|
160
|
+
copts = copts.with_warn_handler(Rc::new(move |event: &sasso::WarnEvent<'_>| {
|
|
161
|
+
handler.borrow_mut().push(Warning::record(event));
|
|
162
|
+
}));
|
|
163
|
+
captured = Some(sink);
|
|
164
|
+
}
|
|
165
|
+
_ => {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// One shape for both entry points: the map is None when it was not asked for.
|
|
169
|
+
let compiled = if want_map {
|
|
170
|
+
sasso::compile_with_source_map(&source, &copts)
|
|
171
|
+
.map(|result| (result.css, Some(result.source_map.to_json())))
|
|
172
|
+
} else {
|
|
173
|
+
sasso::compile(&source, &copts).map(|css| (css, None))
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
let out = ruby.ary_new_capa(4);
|
|
177
|
+
let error = match compiled {
|
|
178
|
+
Ok((css, map_json)) => {
|
|
179
|
+
out.push(css)?;
|
|
180
|
+
out.push(map_json)?;
|
|
181
|
+
None
|
|
182
|
+
}
|
|
183
|
+
// Report the failure as a value; `lib/sasso.rb` raises it, AFTER the
|
|
184
|
+
// warnings below have been delivered.
|
|
185
|
+
Err(e) => {
|
|
186
|
+
out.push(ruby.qnil())?;
|
|
187
|
+
out.push(ruby.qnil())?;
|
|
188
|
+
Some(e.to_string())
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Only ever non-empty under "capture".
|
|
193
|
+
let recorded = captured.map(|sink| sink.take()).unwrap_or_default();
|
|
194
|
+
let warnings = ruby.ary_new_capa(recorded.len());
|
|
195
|
+
for warning in recorded {
|
|
196
|
+
warnings.push(warning.into_hash(ruby)?)?;
|
|
197
|
+
}
|
|
198
|
+
out.push(warnings)?;
|
|
199
|
+
out.push(error)?;
|
|
200
|
+
|
|
201
|
+
Ok(out)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// The bundled core compiler's version, read from the crate this extension is
|
|
205
|
+
/// actually linked against. The core exposes `VERSION` (since 0.9.1) precisely
|
|
206
|
+
/// so a binding cannot report a version that has drifted from its own pin.
|
|
207
|
+
fn core_version() -> &'static str {
|
|
208
|
+
sasso::VERSION
|
|
110
209
|
}
|
|
111
210
|
|
|
112
211
|
#[magnus::init]
|
|
113
212
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
114
213
|
let module = ruby.define_module("Sasso")?;
|
|
115
214
|
let native = module.define_module("Native")?;
|
|
116
|
-
native.define_module_function("_compile", function!(native_compile,
|
|
117
|
-
native.define_module_function("
|
|
215
|
+
native.define_module_function("_compile", function!(native_compile, 2))?;
|
|
216
|
+
native.define_module_function("_core_version", function!(core_version, 0))?;
|
|
118
217
|
Ok(())
|
|
119
218
|
}
|
data/lib/sasso/version.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Sasso
|
|
4
|
-
#
|
|
5
|
-
# native extension pins
|
|
6
|
-
|
|
4
|
+
# Tracks the core `sasso` crate this gem bundles: a release adopting core
|
|
5
|
+
# X.Y.Z is gem X.Y.Z, and the native extension pins that crate exactly
|
|
6
|
+
# (ext/sasso/Cargo.toml). A gem-only fix takes the next patch, so the gem may
|
|
7
|
+
# sit ahead of the crate within a minor — `Sasso::CORE_VERSION` reads the
|
|
8
|
+
# version out of the linked binary and is the authority on what is loaded.
|
|
9
|
+
VERSION = "0.14.0"
|
|
7
10
|
end
|
data/lib/sasso.rb
CHANGED
|
@@ -31,9 +31,21 @@ rescue LoadError
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
module Sasso
|
|
34
|
+
# The bundled compiler crate's version, read from the linked binary rather
|
|
35
|
+
# than written down here, so it cannot drift from what is actually loaded.
|
|
36
|
+
# VERSION tracks it as of 0.14.0, but a gem-only patch moves ahead of it.
|
|
37
|
+
CORE_VERSION = Sasso::Native._core_version.freeze
|
|
38
|
+
|
|
34
39
|
STYLES = %i[expanded compressed].freeze
|
|
35
40
|
SYNTAXES = %i[scss sass css].freeze
|
|
36
41
|
|
|
42
|
+
# The keys of a diagnostic Hash handed to `on_warn:`. `:kind` is :warn or
|
|
43
|
+
# :debug; `:formatted` is the full dart-style block (header, snippet, stack
|
|
44
|
+
# trace) the compiler would otherwise have printed to stderr; `:url` is dart's
|
|
45
|
+
# display form of the source file and `:path` identifies it (the importer's
|
|
46
|
+
# canonical path), which is what tells a dependency from the entry stylesheet.
|
|
47
|
+
WARNING_KEYS = %i[kind deprecation deprecation_id message formatted url line path].freeze
|
|
48
|
+
|
|
37
49
|
module_function
|
|
38
50
|
|
|
39
51
|
# Compile a SCSS/Sass source String to a CSS String.
|
|
@@ -44,20 +56,50 @@ module Sasso
|
|
|
44
56
|
# load_paths: dirs searched for @use/@forward/@import (built-in importer)
|
|
45
57
|
# url: filename shown in diagnostics; ENABLES the dart-exact error block
|
|
46
58
|
# alert_ascii: true => ASCII-only diagnostics (maps to the compiler's no-unicode)
|
|
59
|
+
# charset: false => omit the @charset/BOM prefix on non-ASCII output
|
|
60
|
+
# quiet: true => print no @warn/@debug/deprecation diagnostics at all
|
|
61
|
+
# quiet_deps: true => drop deprecation warnings raised inside dependencies
|
|
62
|
+
# on_warn: a callable receiving each diagnostic as a Hash (see WARNING_KEYS);
|
|
63
|
+
# taking delivery this way replaces the default stderr printing
|
|
47
64
|
#
|
|
65
|
+
# Diagnostics go to $stderr by default, as the `sasso` CLI and dart-sass do.
|
|
48
66
|
# Raises Sasso::CompileError on a compile failure; ArgumentError on bad options.
|
|
49
67
|
def compile_string(source, style: :expanded, syntax: :scss, indented: false,
|
|
50
68
|
load_paths: [], url: nil, alert_ascii: false,
|
|
51
|
-
source_map: false, source_map_include_sources: false
|
|
69
|
+
source_map: false, source_map_include_sources: false,
|
|
70
|
+
charset: true, quiet: false, quiet_deps: false, on_warn: nil)
|
|
52
71
|
syntax = :sass if indented
|
|
53
72
|
validate!(style, STYLES, :style)
|
|
54
73
|
validate!(syntax, SYNTAXES, :syntax)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
# A positional Hash, not keyword arguments: `_compile` is a C function and
|
|
75
|
+
# has no keyword parameters, so the braces say what actually crosses the ABI.
|
|
76
|
+
css, map_json, diagnostics, error = Sasso::Native._compile(String(source), {
|
|
77
|
+
style: style.to_s,
|
|
78
|
+
syntax: syntax.to_s,
|
|
79
|
+
load_paths: Array(load_paths).map(&:to_s),
|
|
80
|
+
url: url && url.to_s,
|
|
81
|
+
unicode: !alert_ascii,
|
|
82
|
+
source_map: source_map,
|
|
83
|
+
source_map_include_sources: source_map_include_sources,
|
|
84
|
+
charset: charset,
|
|
85
|
+
quiet_deps: quiet_deps,
|
|
86
|
+
warnings: warnings_mode(quiet, on_warn),
|
|
87
|
+
})
|
|
88
|
+
# Deliver first, raise second: a compile can warn and then fail, and those
|
|
89
|
+
# warnings are the caller's only copy once `on_warn:` has taken over from
|
|
90
|
+
# the compiler's own printing. The native side hands the failure back as a
|
|
91
|
+
# String for exactly this reason.
|
|
92
|
+
# The `ensure` is what makes the compile failure win when the callable
|
|
93
|
+
# itself raises: a logger that is down should not mask the Sass error, which
|
|
94
|
+
# is the actual news. Ruby records the callable's exception as the
|
|
95
|
+
# CompileError's #cause, so neither is lost.
|
|
96
|
+
begin
|
|
97
|
+
diagnostics.each { |d| on_warn.call(d) } if on_warn
|
|
98
|
+
ensure
|
|
99
|
+
raise CompileError, error if error
|
|
100
|
+
end
|
|
101
|
+
return css unless source_map
|
|
58
102
|
|
|
59
|
-
css, map_json = Sasso::Native._compile_with_map(src, style.to_s, syntax.to_s, paths,
|
|
60
|
-
url && url.to_s, !alert_ascii, source_map_include_sources)
|
|
61
103
|
CompileResult.new(css, JSON.parse(map_json))
|
|
62
104
|
end
|
|
63
105
|
|
|
@@ -87,4 +129,22 @@ module Sasso
|
|
|
87
129
|
"invalid #{name}: #{value.inspect} (expected one of #{allowed.inspect})"
|
|
88
130
|
end
|
|
89
131
|
private_class_method :validate!
|
|
132
|
+
|
|
133
|
+
# Which diagnostic mode the native side installs. "stderr" leaves the
|
|
134
|
+
# compiler's own handler in place — the default costs nothing, and the block
|
|
135
|
+
# prints as it is raised rather than after the compile. The other two modes
|
|
136
|
+
# install a handler, which is what suppresses that printing.
|
|
137
|
+
def warnings_mode(quiet, on_warn)
|
|
138
|
+
if on_warn
|
|
139
|
+
raise ArgumentError, "quiet: and on_warn: are mutually exclusive" if quiet
|
|
140
|
+
raise ArgumentError, "on_warn: must respond to #call" unless on_warn.respond_to?(:call)
|
|
141
|
+
|
|
142
|
+
"capture"
|
|
143
|
+
elsif quiet
|
|
144
|
+
"silence"
|
|
145
|
+
else
|
|
146
|
+
"stderr"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
private_class_method :warnings_mode
|
|
90
150
|
end
|
data/sig/sasso.rbs
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
module Sasso
|
|
2
2
|
VERSION: String
|
|
3
|
+
CORE_VERSION: String
|
|
3
4
|
STYLES: Array[Symbol]
|
|
4
5
|
SYNTAXES: Array[Symbol]
|
|
6
|
+
WARNING_KEYS: Array[Symbol]
|
|
7
|
+
|
|
8
|
+
# One @warn / @debug / deprecation diagnostic, as handed to `on_warn:`.
|
|
9
|
+
type warning = {
|
|
10
|
+
kind: Symbol,
|
|
11
|
+
deprecation: bool,
|
|
12
|
+
deprecation_id: String,
|
|
13
|
+
message: String,
|
|
14
|
+
formatted: String,
|
|
15
|
+
url: String,
|
|
16
|
+
line: Integer,
|
|
17
|
+
path: String,
|
|
18
|
+
}
|
|
5
19
|
|
|
6
20
|
class Error < StandardError
|
|
7
21
|
end
|
|
8
22
|
class CompileError < Error
|
|
9
23
|
end
|
|
10
24
|
|
|
25
|
+
# `Struct.new(:css, :source_map)`. The constructor is spelled out because an
|
|
26
|
+
# RBS class without one inherits `Object`'s zero-argument `new`, which would
|
|
27
|
+
# reject the two-argument call the implementation makes.
|
|
28
|
+
class CompileResult < Struct[untyped]
|
|
29
|
+
attr_reader css: String
|
|
30
|
+
attr_reader source_map: Hash[String, untyped]
|
|
31
|
+
|
|
32
|
+
def initialize: (String css, Hash[String, untyped] source_map) -> void
|
|
33
|
+
end
|
|
34
|
+
|
|
11
35
|
def self.compile_string: (String source, ?style: Symbol, ?syntax: Symbol,
|
|
12
36
|
?indented: bool, ?load_paths: Array[untyped],
|
|
13
|
-
?url: String?, ?alert_ascii: bool
|
|
14
|
-
|
|
37
|
+
?url: String?, ?alert_ascii: bool,
|
|
38
|
+
?source_map: bool, ?source_map_include_sources: bool,
|
|
39
|
+
?charset: bool, ?quiet: bool, ?quiet_deps: bool,
|
|
40
|
+
?on_warn: (^(warning) -> void)?) -> (String | CompileResult)
|
|
41
|
+
def self.compile: (untyped path, **untyped opts) -> (String | CompileResult)
|
|
15
42
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sasso
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- momiji-rs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|
|
@@ -102,7 +102,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
102
102
|
requirements:
|
|
103
103
|
- - ">="
|
|
104
104
|
- !ruby/object:Gem::Version
|
|
105
|
-
version: 3.
|
|
105
|
+
version: 3.2.0
|
|
106
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - ">="
|