rewrite parsing, evaluation, and serialization (#67)

Adds support for the indented syntax, plain CSS imports, `@forward`, and many other previously missing features.
This commit is contained in:
Connor Skees 2022-12-26 15:33:04 -05:00 committed by GitHub
parent b913eabdf1
commit ffaee04613
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
194 changed files with 21407 additions and 15873 deletions

View File

@ -1,19 +1,20 @@
--- ---
name: Incorrect SASS Output name: Incorrect Sass Output
about: There exists a differential between the output of grass and dart-sass about: `grass` and `dart-sass` differ in output or `grass` reports and error for a valid style sheet
title: '' title: ''
labels: bug labels: bug
assignees: connorskees assignees: connorskees
--- ---
**Minimal Reproducible Example**: **Failing Sass**:
``` ```
a { a {
color: red; color: red;
} }
``` ```
<!-- Showing output from both tools is optional, but does help in debugging -->
**`grass` Output**: **`grass` Output**:
``` ```
a { a {

View File

@ -73,10 +73,10 @@ jobs:
- name: Build - name: Build
run: cargo build run: cargo build
- name: Install dart-sass 1.36.0 - name: Install dart-sass 1.54.3
run: | run: |
wget https://github.com/sass/dart-sass/releases/download/1.36.0/dart-sass-1.36.0-linux-x64.tar.gz wget https://github.com/sass/dart-sass/releases/download/1.54.3/dart-sass-1.54.3-linux-x64.tar.gz
tar -xzvf dart-sass-1.36.0-linux-x64.tar.gz tar -xzvf dart-sass-1.54.3-linux-x64.tar.gz
- name: Install bootstrap - name: Install bootstrap
run: git clone --depth=1 --branch v5.0.2 https://github.com/twbs/bootstrap.git run: git clone --depth=1 --branch v5.0.2 https://github.com/twbs/bootstrap.git

View File

@ -1,3 +1,55 @@
# TBD
- complete rewrite of parsing, evaluation, and serialization steps
- **implement the indented syntax**
- **implement plain CSS imports**
- support for custom properties
- represent all numbers as f64, rather than using arbitrary precision
- implement media query merging
- implement builtin function `keywords`
- implement Infinity and -Infinity
- implement the `@forward` rule
- feature complete parsing of `@supports` conditions
- support media queries level 4
- implement calculation simplification and the calculation value type
- implement builtin fns `calc-args`, `calc-name`
- add builtin math module variables `$epsilon`, `$max-safe-integer`, `$min-safe-integer`, `$max-number`, `$min-number`
- allow angle units `turn` and `grad` in builtin trigonometry functions
- implement `@at-root` conditions
- implement `@import` conditions
- remove dependency on `num-rational` and `beef`
- support control flow inside declaration blocks
For example:
```scss
a {
-webkit-: {
@if 1 == 1 {
scrollbar: red
}
}
}
```
will now emit
```css
a {
-webkit-scrollbar: red;
}
```
- always emit `rgb`/`rgba`/`hsl`/`hsla` for colors declared as such in expanded mode
- more efficiently compress colors in compressed mode
- treat `:where` the same as `:is` in extension
- support "import-only" files
- treat `@elseif` the same as `@else if`
- implement division of non-comparable units and feature complete support for complex units
- support 1 arg color.hwb()
UPCOMING:
- error when `@extend` is used across `@media` boundaries
- more robust support for NaN in builtin functions
# 0.11.2 # 0.11.2
- make `grass::Error` a `Send` type - make `grass::Error` a `Send` type

View File

@ -23,63 +23,32 @@ path = "src/lib.rs"
# crate-type = ["cdylib", "rlib"] # crate-type = ["cdylib", "rlib"]
bench = false bench = false
[[bench]]
path = "benches/variables.rs"
name = "variables"
harness = false
[[bench]]
path = "benches/colors.rs"
name = "colors"
harness = false
[[bench]]
path = "benches/numbers.rs"
name = "numbers"
harness = false
[[bench]]
path = "benches/control_flow.rs"
name = "control_flow"
harness = false
[[bench]]
path = "benches/styles.rs"
name = "styles"
harness = false
[dependencies] [dependencies]
clap = { version = "2.34.0", optional = true } clap = { version = "2.34.0", optional = true }
num-rational = "0.4" # todo: use lazy_static
num-bigint = "0.4"
num-traits = "0.2.14"
once_cell = "1.15.0" once_cell = "1.15.0"
# todo: use xorshift for random numbers
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
# todo: update to use asref<path>
# todo: update to expose more info (for eww)
# todo: update to use text_size::TextRange
codemap = "0.1.3" codemap = "0.1.3"
wasm-bindgen = { version = "0.2.68", optional = true } wasm-bindgen = { version = "0.2.68", optional = true }
beef = "0.5" # todo: use phf for global functions
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
# criterion is not a dev-dependency because it makes tests take too indexmap = "1.9.0"
# long to compile, and you cannot make dev-dependencies optional # todo: do we really need interning for things?
criterion = { version = "0.4.0", optional = true }
indexmap = "1.9.1"
lasso = "0.6" lasso = "0.6"
[features] [features]
# todo: no commandline by default
default = ["commandline", "random"] default = ["commandline", "random"]
# Option (enabled by default): build a binary using clap # Option (enabled by default): build a binary using clap
commandline = ["clap"] commandline = ["clap"]
# Option: enable nightly-only features (for right now, only the `track_caller` attribute)
nightly = []
# Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()` # Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()`
random = ["rand"] random = ["rand"]
# Option: expose JavaScript-friendly WebAssembly exports # Option: expose JavaScript-friendly WebAssembly exports
wasm-exports = ["wasm-bindgen"] wasm-exports = ["wasm-bindgen"]
# Option: enable features that assist in profiling (e.g. inline(never))
profiling = []
# Option: enable criterion for benchmarking
bench = ["criterion"]
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3.0"

View File

@ -3,17 +3,12 @@
This crate aims to provide a high level interface for compiling Sass into This crate aims to provide a high level interface for compiling Sass into
plain CSS. It offers a very limited API, currently exposing only 2 functions. plain CSS. It offers a very limited API, currently exposing only 2 functions.
In addition to a library, also included is a binary that is intended to act as an invisible In addition to a library, this crate also includes a binary that is intended to act as an invisible
replacement to the Sass commandline executable. replacement to the Sass commandline executable.
This crate aims to achieve complete feature parity with the `dart-sass` reference This crate aims to achieve complete feature parity with the `dart-sass` reference
implementation. A deviation from the `dart-sass` implementation can be considered implementation. A deviation from the `dart-sass` implementation can be considered
a bug except for in the following situations: a bug except for in the case of error message and error spans.
- Error messages
- Error spans
- Certain aspects of the indented syntax
- Potentially others in the future
[Documentation](https://docs.rs/grass/) [Documentation](https://docs.rs/grass/)
[crates.io](https://crates.io/crates/grass) [crates.io](https://crates.io/crates/grass)
@ -24,17 +19,7 @@ a bug except for in the following situations:
Every commit of `grass` is tested against bootstrap v5.0.2, and every release is tested against the last 2,500 commits of bootstrap's `main` branch. Every commit of `grass` is tested against bootstrap v5.0.2, and every release is tested against the last 2,500 commits of bootstrap's `main` branch.
That said, there are a number of known missing features and bugs. The notable features remaining are That said, there are a number of known missing features and bugs. The rough edges of `grass` largely include `@forward` and more complex uses of `@uses`. We support basic usage of these rules, but more advanced features such as `@import`ing modules containing `@forward` with prefixes may not behave as expected.
```
indented syntax
@forward and more complex uses of @use
@at-root and @import media queries
@media query merging
/ as a separator in color functions, e.g. rgba(255, 255, 255 / 0)
Infinity and -Infinity
builtin meta function `keywords`
```
All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19). All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19).
@ -44,11 +29,10 @@ All known missing features and bugs are tracked in [#19](https://github.com/conn
`grass` experimentally releases a `grass` experimentally releases a
[WASM version of the library to npm](https://www.npmjs.com/package/@connorskees/grass), [WASM version of the library to npm](https://www.npmjs.com/package/@connorskees/grass),
compiled using wasm-bindgen. To use `grass` in your JavaScript projects, just run compiled using wasm-bindgen. To use `grass` in your JavaScript projects, run
`npm install @connorskees/grass` to add it to your package.json. Better documentation `npm install @connorskees/grass` to add it to your package.json. This version of grass is not currently well documented, but one can find example usage in the [`grassmeister` repository](https://github.com/connorskees/grassmeister).
for this version will be provided when the library becomes more stable.
## Features ## Cargo Features
### commandline ### commandline
@ -73,28 +57,16 @@ are in the official spec.
Having said that, to run the official test suite, Having said that, to run the official test suite,
```bash
git clone https://github.com/connorskees/grass --recursive
cd grass
cargo b --release
./sass-spec/sass-spec.rb -c './target/release/grass'
```
Note: you will have to install [ruby](https://www.ruby-lang.org/en/downloads/),
[bundler](https://bundler.io/) and run `bundle install` in `./sass-spec/`.
This might also require you to install the requirements separately
for [curses](https://github.com/ruby/curses).
Alternatively, it is possible to use nodejs to run the spec,
```bash ```bash
# This script expects node >=v14.14.0. Check version with `node --version` # This script expects node >=v14.14.0. Check version with `node --version`
git clone https://github.com/connorskees/grass --recursive git clone https://github.com/connorskees/grass --recursive
cd grass && cargo b --release cd grass && cargo b --release
cd sass-spec && npm install cd sass-spec && npm install
npm run sass-spec -- --command '../target/release/grass' npm run sass-spec -- --impl=dart-sass --command '../target/release/grass'
``` ```
The spec runner does not work on Windows.
These numbers come from a default run of the Sass specification as shown above. These numbers come from a default run of the Sass specification as shown above.
``` ```
@ -103,3 +75,5 @@ PASSING: 4205
FAILING: 2051 FAILING: 2051
TOTAL: 6256 TOTAL: 6256
``` ```
<!-- todo: msrv 1.41.1 -->

View File

@ -1,5 +0,0 @@
@for $i from 0 to 250 {
a {
color: $i;
}
}

View File

@ -1,26 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
pub fn many_hsla(c: &mut Criterion) {
c.bench_function("many_hsla", |b| {
b.iter(|| {
grass::from_string(
black_box(include_str!("many_hsla.scss").to_string()),
&Default::default(),
)
})
});
}
pub fn many_named_colors(c: &mut Criterion) {
c.bench_function("many_named_colors", |b| {
b.iter(|| {
grass::from_string(
black_box(include_str!("many_named_colors.scss").to_string()),
&Default::default(),
)
})
});
}
criterion_group!(benches, many_hsla, many_named_colors,);
criterion_main!(benches);

View File

@ -1,11 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use grass::StyleSheet;
pub fn big_for(c: &mut Criterion) {
c.bench_function("big_for", |b| {
b.iter(|| StyleSheet::new(black_box(include_str!("big_for.scss").to_string())))
});
}
criterion_group!(benches, big_for);
criterion_main!(benches);

View File

@ -1,77 +0,0 @@
a {
color: 0.45684318453159234;
color: 0.32462456760120406;
color: 0.8137736535327419;
color: 0.7358225117215007;
color: 0.17214528398099915;
color: 0.49902566583569585;
color: 0.338644100262644;
color: 0.20366595024608847;
color: 0.9913235248842889;
color: 0.4504985674365235;
color: 0.4019760103825616;
color: 0.050337450640631;
color: 0.5651205053784689;
color: 0.3858205416141207;
color: 0.09217890891037928;
color: 0.6435125135923638;
color: 0.202134723711479;
color: 0.11994222382746123;
color: 0.47986245642426784;
color: 0.31377775364535687;
color: 0.020494291726303793;
color: 0.7036980462009633;
color: 0.05224790970717974;
color: 0.4725031661423096;
color: 0.1799319597283685;
color: 0.5766381901433899;
color: 0.29587586101578056;
color: 0.89900436907659;
color: 0.6382187357736526;
color: 0.34077453754121845;
color: 0.3316247621124896;
color: 0.8886550774121025;
color: 0.9579727032842532;
color: 0.13260213335114324;
color: 0.5036670768341907;
color: 0.7338168132118498;
color: 0.011390676385644283;
color: 0.9303733599096669;
color: 0.24485375467577541;
color: 0.13029227061645976;
color: 0.8867174997526868;
color: 0.526450140183167;
color: 0.4183622224634642;
color: 0.38194907182912086;
color: 0.95989056158538;
color: 0.18671819783650978;
color: 0.631670113474244;
color: 0.28215806751639927;
color: 0.744551857407553;
color: 0.16364787204458753;
color: 0.8854899624202007;
color: 0.6356831607592164;
color: 0.803995697660223;
color: 0.5474581871155357;
color: 0.33488378257527607;
color: 0.8364000760499766;
color: 0.5518853083384915;
color: 0.141798633391226;
color: 0.9094555423407225;
color: 0.8708920525327435;
color: 0.5211086312895997;
color: 0.7287295949985033;
color: 0.11874756345245452;
color: 0.1737295194329479;
color: 0.2789643462534729;
color: 0.9493428424418854;
color: 0.450286842379213;
color: 0.08050497611874319;
color: 0.5585676334291367;
color: 0.8228926312982258;
color: 0.40546086577035834;
color: 0.3837833877800164;
color: 0.2933238166508011;
color: 0.22631956793343344;
color: 0.9693016209486633;
}

View File

@ -1,502 +0,0 @@
a {
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
color: foo;
}

View File

@ -1,27 +0,0 @@
a {
color: hsla(222.2192777206995, 110.3692139794996, 61.81051067574404, 0.534401685087422);
color: hsla(11.352992797547246, 74.664898057584, 6.382261199759358, 0.4874777440386838);
color: hsla(132.425922219443, 29.256511738803592, 24.89540771728558, 0.5596738032089829);
color: hsla(13.309525399907187, 30.18831771387657, 85.24254752668342, 0.5639756616594267);
color: hsla(115.80418431138013, 138.80875075306716, 23.490066034361682, 0.9475768360338274);
color: hsla(60.46239828401012, 10.313109482705745, 105.16963702630053, 0.6042021161827366);
color: hsla(34.30938279662815, 1.889472112004964, 25.15291728283431, 0.9511924190787797);
color: hsla(98.71285284443937, 23.776914475219797, 32.648612555008434, 0.977536897763227);
color: hsla(102.10433890385715, 63.77885767681341, 16.646770070167822, 0.6574613239168576);
color: hsla(57.92186087245385, 60.13034947932598, 68.54893513559583, 0.373803434079244);
color: hsla(269.9538004245578, 52.78619311546282, 110.12893163260173, 0.576868671613627);
color: hsla(156.9093802116642, 124.93331830547281, 19.561761686688804, 0.3974323561380795);
color: hsla(19.511009502958405, 9.985975717432698, 1.7222436103076566, 0.6185271078002709);
color: hsla(15.16885447767802, 65.20912769433798, 276.68448067449, 0.24252634099912806);
color: hsla(104.5304367379402, 48.4396743669759, 87.36931435860792, 0.49110860679749657);
color: hsla(1.23896746147205, 8.983910503200377, 40.0155021692319, 0.5083377501566763);
color: hsla(115.17557319839848, 0.7026571842435614, 13.941266396527283, 0.2702835740499192);
color: hsla(39.85503118282452, 132.112827958992, 10.699003040970583, 0.1682327962372605);
color: hsla(6.928648113302179, 170.32675719792294, 8.03447559763514, 0.355029719268528);
color: hsla(139.7730609176561, 168.9185250475494, 77.0568608336116, 0.20722154573547713);
color: hsla(14.530537405142663, 15.039646435716925, 33.36286228624303, 0.667781746780932);
color: hsla(8.919544897442268, 100.64466379531277, 76.11409137494536, 0.05256078867970626);
color: hsla(2.495018335398737, 132.07029268437287, 27.340212426881667, 0.6728327813869602);
color: hsla(194.07056581458647, 106.38402415451384, 71.26432187392453, 0.9217222550714675);
color: hsla(192.1411495274188, 50.30798166678871, 57.471447627549466, 0.902530813592693);
}

View File

@ -1,102 +0,0 @@
a {
color: 19347854542143253997;
color: 60451179538202003956;
color: 22288810622252242572;
color: 58286097263656413001;
color: 33530395320173540432;
color: 95316889258994145183;
color: 3358232388665482383;
color: 12815224243568515372;
color: 19283136184153758095;
color: 33122315732214482743;
color: 9996867737128598424;
color: 36386708368650762713;
color: 84580393574048587326;
color: 74014760739403436469;
color: 60107853553778145259;
color: 81683811639015395122;
color: 34504089788874431363;
color: 86195730836778181984;
color: 13776202879273515695;
color: 8735653406911714530;
color: 61456379490169639279;
color: 35762072210651926818;
color: 61460373231570207917;
color: 8829619883393927322;
color: 63569570958925796423;
color: 31230143064617844098;
color: 75979523766937051158;
color: 58391697254842578722;
color: 38067688303474176911;
color: 42782465660343407165;
color: 28649126023011966552;
color: 31336334901261132629;
color: 55578332748910725911;
color: 77992165774520153730;
color: 26983857367747718497;
color: 50314775931055603758;
color: 79919191917674804059;
color: 6065254172667046510;
color: 48812533786990556987;
color: 50626029581432907748;
color: 32920927533084228447;
color: 25766668522608879151;
color: 36211419497217659201;
color: 46555121092629591544;
color: 66786464197999028486;
color: 770851654700876789;
color: 49316288886854275054;
color: 49763156148705535619;
color: 23640662784051843734;
color: 55149907392031692997;
color: 10742779066449549077;
color: 30635120383894071220;
color: 32890775075954786908;
color: 40495273332944532421;
color: 42981753734717358017;
color: 63273869431589510509;
color: 84115199502821252378;
color: 77677063244691653966;
color: 79531949286819070087;
color: 24520937278436624652;
color: 67527137685435042183;
color: 10022950353544463594;
color: 25295880679144540312;
color: 71085411926567716829;
color: 85811798388998243673;
color: 89065727393019327572;
color: 38705291487309161676;
color: 16925562774622731569;
color: 57721458592958125990;
color: 88102301592786125794;
color: 93210380268033017386;
color: 47256079955519109374;
color: 59093710890759331173;
color: 24855561476278918903;
color: 93239261909263353253;
color: 82315430173632275592;
color: 40813136216283356603;
color: 5028624138667579466;
color: 93353049610249570578;
color: 33571801430120811399;
color: 24369596975910994936;
color: 54440817408523476491;
color: 8774430875523255402;
color: 73543734840226713059;
color: 84538041799079500728;
color: 4985228934843777484;
color: 92982844718976486431;
color: 99181986425678886553;
color: 61661316527868010659;
color: 73884691993026466740;
color: 61205542045935672699;
color: 56700006318786104676;
color: 56517700553046170346;
color: 53931185440468841623;
color: 66376069944981888390;
color: 30341154629821911856;
color: 26359842299201187881;
color: 13977447700076976060;
color: 67153963281824267639;
color: 75242965964153682009;
}

View File

@ -1,502 +0,0 @@
a {
color: mediumvioletred;
color: burlywood;
color: deeppink;
color: lavenderblush;
color: steelblue;
color: lightslategray;
color: palevioletred;
color: rosybrown;
color: whitesmoke;
color: navy;
color: blue;
color: darkolivegreen;
color: transparent;
color: black;
color: lightskyblue;
color: sandybrown;
color: darkturquoise;
color: darkorange;
color: tan;
color: tomato;
color: lightgray;
color: seagreen;
color: cadetblue;
color: crimson;
color: darksalmon;
color: mediumpurple;
color: mistyrose;
color: cornflowerblue;
color: gray;
color: lightyellow;
color: purple;
color: darkred;
color: mediumturquoise;
color: rosybrown;
color: sandybrown;
color: mediumblue;
color: darkgoldenrod;
color: lightgreen;
color: aquamarine;
color: linen;
color: pink;
color: oldlace;
color: lightgoldenrodyellow;
color: chocolate;
color: lightblue;
color: mediumseagreen;
color: honeydew;
color: powderblue;
color: floralwhite;
color: royalblue;
color: greenyellow;
color: lightyellow;
color: beige;
color: thistle;
color: dodgerblue;
color: navajowhite;
color: lightseagreen;
color: saddlebrown;
color: moccasin;
color: turquoise;
color: purple;
color: darkgray;
color: thistle;
color: mistyrose;
color: salmon;
color: palegoldenrod;
color: white;
color: cornflowerblue;
color: lightyellow;
color: snow;
color: aqua;
color: indianred;
color: lightyellow;
color: darkkhaki;
color: aqua;
color: darkviolet;
color: powderblue;
color: darkblue;
color: papayawhip;
color: hotpink;
color: chocolate;
color: mediumaquamarine;
color: lightskyblue;
color: mediumvioletred;
color: white;
color: lightgreen;
color: palevioletred;
color: antiquewhite;
color: indianred;
color: darkgreen;
color: darkmagenta;
color: darkviolet;
color: snow;
color: lightgoldenrodyellow;
color: darksalmon;
color: royalblue;
color: cornsilk;
color: deepskyblue;
color: lightseagreen;
color: skyblue;
color: mediumblue;
color: azure;
color: firebrick;
color: turquoise;
color: plum;
color: aqua;
color: chocolate;
color: lightyellow;
color: coral;
color: darkseagreen;
color: antiquewhite;
color: cornflowerblue;
color: chartreuse;
color: darkcyan;
color: snow;
color: honeydew;
color: tomato;
color: darkturquoise;
color: papayawhip;
color: lightskyblue;
color: honeydew;
color: cornflowerblue;
color: darkgray;
color: mediumseagreen;
color: thistle;
color: darkgoldenrod;
color: forestgreen;
color: black;
color: cornflowerblue;
color: blanchedalmond;
color: aliceblue;
color: mediumblue;
color: blueviolet;
color: coral;
color: dodgerblue;
color: whitesmoke;
color: yellow;
color: burlywood;
color: whitesmoke;
color: bisque;
color: palegreen;
color: darkblue;
color: fuchsia;
color: darkviolet;
color: orangered;
color: thistle;
color: darkkhaki;
color: mediumpurple;
color: lightslategray;
color: wheat;
color: brown;
color: oldlace;
color: mintcream;
color: ivory;
color: gold;
color: forestgreen;
color: black;
color: darkorchid;
color: springgreen;
color: mediumvioletred;
color: navajowhite;
color: aquamarine;
color: crimson;
color: dodgerblue;
color: slateblue;
color: lawngreen;
color: lightgray;
color: peachpuff;
color: lightgreen;
color: yellow;
color: gold;
color: silver;
color: lightblue;
color: bisque;
color: mediumorchid;
color: violet;
color: darkturquoise;
color: steelblue;
color: black;
color: palegoldenrod;
color: gray;
color: khaki;
color: linen;
color: purple;
color: skyblue;
color: beige;
color: ghostwhite;
color: saddlebrown;
color: yellow;
color: dimgray;
color: floralwhite;
color: lightgray;
color: powderblue;
color: aquamarine;
color: black;
color: lightgray;
color: olive;
color: darkkhaki;
color: darkmagenta;
color: darkturquoise;
color: ghostwhite;
color: turquoise;
color: blue;
color: darkorange;
color: oldlace;
color: saddlebrown;
color: lightcoral;
color: fuchsia;
color: olivedrab;
color: seagreen;
color: dodgerblue;
color: ghostwhite;
color: antiquewhite;
color: indianred;
color: honeydew;
color: antiquewhite;
color: darkorchid;
color: gainsboro;
color: whitesmoke;
color: hotpink;
color: indianred;
color: lightgoldenrodyellow;
color: mintcream;
color: peachpuff;
color: goldenrod;
color: orangered;
color: skyblue;
color: plum;
color: slateblue;
color: mediumslateblue;
color: olivedrab;
color: indigo;
color: lightgoldenrodyellow;
color: red;
color: lemonchiffon;
color: bisque;
color: crimson;
color: cadetblue;
color: mediumblue;
color: orange;
color: darkslateblue;
color: olivedrab;
color: violet;
color: mediumspringgreen;
color: indigo;
color: moccasin;
color: lightpink;
color: deepskyblue;
color: oldlace;
color: lightsalmon;
color: mediumturquoise;
color: darksalmon;
color: darkblue;
color: dimgray;
color: blanchedalmond;
color: mediumturquoise;
color: black;
color: peachpuff;
color: olivedrab;
color: darkgreen;
color: white;
color: paleturquoise;
color: aliceblue;
color: limegreen;
color: darkslateblue;
color: skyblue;
color: darksalmon;
color: salmon;
color: darkcyan;
color: pink;
color: saddlebrown;
color: blue;
color: blue;
color: papayawhip;
color: mediumvioletred;
color: darksalmon;
color: darkolivegreen;
color: yellowgreen;
color: wheat;
color: darkslategray;
color: purple;
color: red;
color: mistyrose;
color: palegreen;
color: cornflowerblue;
color: seashell;
color: mediumpurple;
color: darkslateblue;
color: honeydew;
color: chocolate;
color: ivory;
color: mediumslateblue;
color: darkturquoise;
color: navajowhite;
color: red;
color: sienna;
color: gray;
color: cadetblue;
color: silver;
color: burlywood;
color: cornflowerblue;
color: palegoldenrod;
color: yellow;
color: chocolate;
color: darkseagreen;
color: lightpink;
color: chocolate;
color: tomato;
color: thistle;
color: tomato;
color: whitesmoke;
color: indianred;
color: lightgreen;
color: peru;
color: orange;
color: palegoldenrod;
color: darkkhaki;
color: olive;
color: chocolate;
color: gainsboro;
color: chocolate;
color: oldlace;
color: royalblue;
color: dodgerblue;
color: darkmagenta;
color: saddlebrown;
color: beige;
color: floralwhite;
color: aliceblue;
color: aquamarine;
color: mintcream;
color: mintcream;
color: palegreen;
color: yellow;
color: lightsteelblue;
color: salmon;
color: darkviolet;
color: whitesmoke;
color: salmon;
color: violet;
color: aliceblue;
color: mediumspringgreen;
color: firebrick;
color: goldenrod;
color: gold;
color: honeydew;
color: lawngreen;
color: azure;
color: ghostwhite;
color: lightsalmon;
color: oldlace;
color: lime;
color: indigo;
color: saddlebrown;
color: mediumaquamarine;
color: rosybrown;
color: gray;
color: seashell;
color: midnightblue;
color: slateblue;
color: snow;
color: wheat;
color: indigo;
color: tomato;
color: lightyellow;
color: cornflowerblue;
color: lightgray;
color: slategray;
color: steelblue;
color: skyblue;
color: oldlace;
color: darkseagreen;
color: lawngreen;
color: gainsboro;
color: aquamarine;
color: snow;
color: royalblue;
color: dimgray;
color: orangered;
color: forestgreen;
color: honeydew;
color: darksalmon;
color: chartreuse;
color: mediumblue;
color: mediumpurple;
color: lightyellow;
color: deeppink;
color: darkgreen;
color: peachpuff;
color: mintcream;
color: mediumblue;
color: sandybrown;
color: green;
color: darkolivegreen;
color: crimson;
color: darkslateblue;
color: rosybrown;
color: blueviolet;
color: darkgray;
color: transparent;
color: darkslategray;
color: lightcyan;
color: honeydew;
color: teal;
color: brown;
color: darkorchid;
color: fuchsia;
color: lime;
color: mediumpurple;
color: darkorange;
color: midnightblue;
color: mediumvioletred;
color: limegreen;
color: lightseagreen;
color: mistyrose;
color: burlywood;
color: wheat;
color: maroon;
color: darkgoldenrod;
color: hotpink;
color: lightskyblue;
color: darkgreen;
color: yellowgreen;
color: mintcream;
color: navy;
color: oldlace;
color: papayawhip;
color: powderblue;
color: lightskyblue;
color: lightyellow;
color: yellowgreen;
color: deepskyblue;
color: purple;
color: lemonchiffon;
color: darkgoldenrod;
color: lightskyblue;
color: salmon;
color: snow;
color: darkgoldenrod;
color: azure;
color: lightpink;
color: bisque;
color: palegreen;
color: darkviolet;
color: slateblue;
color: blue;
color: orchid;
color: ghostwhite;
color: lavender;
color: lavenderblush;
color: cornsilk;
color: teal;
color: lightcyan;
color: darkslategray;
color: powderblue;
color: lightyellow;
color: powderblue;
color: bisque;
color: tomato;
color: ghostwhite;
color: papayawhip;
color: thistle;
color: firebrick;
color: mediumspringgreen;
color: darkkhaki;
color: indigo;
color: azure;
color: chartreuse;
color: whitesmoke;
color: forestgreen;
color: darkorange;
color: darkslategray;
color: honeydew;
color: dodgerblue;
color: skyblue;
color: mediumspringgreen;
color: olivedrab;
color: greenyellow;
color: wheat;
color: seagreen;
color: crimson;
color: lavender;
color: steelblue;
color: aliceblue;
color: chartreuse;
color: orangered;
color: transparent;
color: dimgray;
color: palegreen;
color: forestgreen;
color: mediumorchid;
color: darkorchid;
color: pink;
color: aliceblue;
color: greenyellow;
color: darkslategray;
color: paleturquoise;
color: lightslategray;
color: darkturquoise;
color: sandybrown;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,254 +0,0 @@
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
$foo-bar: foo;
a {
color: $foo-bar;
}

View File

@ -1,27 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use grass::StyleSheet;
pub fn many_floats(c: &mut Criterion) {
c.bench_function("many_floats", |b| {
b.iter(|| StyleSheet::new(black_box(include_str!("many_floats.scss").to_string())))
});
}
pub fn many_integers(c: &mut Criterion) {
c.bench_function("many_integers", |b| {
b.iter(|| StyleSheet::new(black_box(include_str!("many_integers.scss").to_string())))
});
}
pub fn many_small_integers(c: &mut Criterion) {
c.bench_function("many_small_integers", |b| {
b.iter(|| {
StyleSheet::new(black_box(
include_str!("many_small_integers.scss").to_string(),
))
})
});
}
criterion_group!(benches, many_floats, many_integers, many_small_integers);
criterion_main!(benches);

View File

@ -1,11 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use grass::StyleSheet;
pub fn many_foo(c: &mut Criterion) {
c.bench_function("many_foo", |b| {
b.iter(|| StyleSheet::new(black_box(include_str!("many_foo.scss").to_string())))
});
}
criterion_group!(benches, many_foo);
criterion_main!(benches);

View File

@ -1,15 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use grass::StyleSheet;
pub fn many_variable_redeclarations(c: &mut Criterion) {
c.bench_function("many_variable_redeclarations", |b| {
b.iter(|| {
StyleSheet::new(black_box(
include_str!("many_variable_redeclarations.scss").to_string(),
))
})
});
}
criterion_group!(benches, many_variable_redeclarations);
criterion_main!(benches);

View File

@ -1,3 +1,3 @@
body { a {
background: red; color: red;
} }

@ -1 +1 @@
Subproject commit e348959657f1e274cef658283436a311a925a673 Subproject commit f7265276e53b0c5e6df0f800ed4b0ae61fbd0351

View File

@ -1,237 +0,0 @@
use std::collections::HashMap;
use codemap::{Span, Spanned};
use crate::{
common::Identifier,
error::SassResult,
value::Value,
{Cow, Token},
};
#[derive(Debug, Clone)]
pub(crate) struct FuncArgs(pub Vec<FuncArg>);
#[derive(Debug, Clone)]
pub(crate) struct FuncArg {
pub name: Identifier,
pub default: Option<Vec<Token>>,
pub is_variadic: bool,
}
impl FuncArgs {
pub const fn new() -> Self {
FuncArgs(Vec::new())
}
pub fn len(&self) -> usize {
self.0.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug, Clone)]
pub(crate) struct CallArgs(pub HashMap<CallArg, SassResult<Spanned<Value>>>, pub Span);
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub(crate) enum CallArg {
Named(Identifier),
Positional(usize),
}
impl CallArg {
pub fn position(&self) -> Result<usize, String> {
match self {
Self::Named(ref name) => Err(name.to_string()),
Self::Positional(p) => Ok(*p),
}
}
pub fn decrement(self) -> CallArg {
match self {
Self::Named(..) => self,
Self::Positional(p) => Self::Positional(p - 1),
}
}
}
impl CallArgs {
pub fn new(span: Span) -> Self {
CallArgs(HashMap::new(), span)
}
pub fn to_css_string(self, is_compressed: bool) -> SassResult<Spanned<String>> {
let mut string = String::with_capacity(2 + self.len() * 10);
string.push('(');
let mut span = self.1;
if self.is_empty() {
return Ok(Spanned {
node: "()".to_owned(),
span,
});
}
let args = match self.get_variadic() {
Ok(v) => v,
Err(..) => {
return Err(("Plain CSS functions don't support keyword arguments.", span).into())
}
};
string.push_str(
&args
.iter()
.map(|a| {
span = span.merge(a.span);
a.node.to_css_string(a.span, is_compressed)
})
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
.join(", "),
);
string.push(')');
Ok(Spanned { node: string, span })
}
/// Get argument by name
///
/// Removes the argument
pub fn get_named<T: Into<Identifier>>(&mut self, val: T) -> Option<SassResult<Spanned<Value>>> {
self.0.remove(&CallArg::Named(val.into()))
}
/// Get a positional argument by 0-indexed position
///
/// Removes the argument
pub fn get_positional(&mut self, val: usize) -> Option<SassResult<Spanned<Value>>> {
self.0.remove(&CallArg::Positional(val))
}
pub fn get<T: Into<Identifier>>(
&mut self,
position: usize,
name: T,
) -> Option<SassResult<Spanned<Value>>> {
match self.get_named(name) {
Some(v) => Some(v),
None => self.get_positional(position),
}
}
pub fn get_err(&mut self, position: usize, name: &'static str) -> SassResult<Value> {
match self.get_named(name) {
Some(v) => Ok(v?.node),
None => match self.get_positional(position) {
Some(v) => Ok(v?.node),
None => Err((format!("Missing argument ${}.", name), self.span()).into()),
},
}
}
/// Decrement all positional arguments by 1
///
/// This is used by builtin function `call` to pass
/// positional arguments to the other function
pub fn decrement(self) -> Self {
CallArgs(
self.0
.into_iter()
.map(|(k, v)| (k.decrement(), v))
.collect(),
self.1,
)
}
pub const fn span(&self) -> Span {
self.1
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn min_args(&self, min: usize) -> SassResult<()> {
let len = self.len();
if len < min {
if min == 1 {
return Err(("At least one argument must be passed.", self.span()).into());
}
todo!("min args greater than one")
}
Ok(())
}
pub fn max_args(&self, max: usize) -> SassResult<()> {
let len = self.len();
if len > max {
let mut err = String::with_capacity(50);
#[allow(clippy::format_push_string)]
err.push_str(&format!("Only {} argument", max));
if max != 1 {
err.push('s');
}
err.push_str(" allowed, but ");
err.push_str(&len.to_string());
err.push(' ');
if len == 1 {
err.push_str("was passed.");
} else {
err.push_str("were passed.");
}
return Err((err, self.span()).into());
}
Ok(())
}
pub fn default_arg(
&mut self,
position: usize,
name: &'static str,
default: Value,
) -> SassResult<Value> {
Ok(match self.get(position, name) {
Some(val) => val?.node,
None => default,
})
}
pub fn positional_arg(&mut self, position: usize) -> Option<SassResult<Spanned<Value>>> {
self.get_positional(position)
}
pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> SassResult<Value> {
Ok(match self.get_named(name) {
Some(val) => val?.node,
None => default,
})
}
pub fn get_variadic(self) -> SassResult<Vec<Spanned<Value>>> {
let mut vals = Vec::new();
let mut args = match self
.0
.into_iter()
.map(|(a, v)| Ok((a.position()?, v)))
.collect::<Result<Vec<(usize, SassResult<Spanned<Value>>)>, String>>()
{
Ok(v) => v,
Err(e) => return Err((format!("No argument named ${}.", e), self.1).into()),
};
args.sort_by(|(a1, _), (a2, _)| a1.cmp(a2));
for (_, arg) in args {
vals.push(arg?);
}
Ok(vals)
}
}

298
src/ast/args.rs Normal file
View File

@ -0,0 +1,298 @@
use std::{
collections::{BTreeMap, BTreeSet},
iter::Iterator,
mem,
};
use codemap::{Span, Spanned};
use crate::{
common::{Identifier, ListSeparator},
error::SassResult,
utils::to_sentence,
value::Value,
};
use super::AstExpr;
#[derive(Debug, Clone)]
pub(crate) struct Argument {
pub name: Identifier,
pub default: Option<AstExpr>,
}
#[derive(Debug, Clone)]
pub(crate) struct ArgumentDeclaration {
pub args: Vec<Argument>,
pub rest: Option<Identifier>,
}
impl ArgumentDeclaration {
pub fn empty() -> Self {
Self {
args: Vec::new(),
rest: None,
}
}
pub fn verify<T>(
&self,
num_positional: usize,
names: &BTreeMap<Identifier, T>,
span: Span,
) -> SassResult<()> {
let mut named_used = 0;
for i in 0..self.args.len() {
let argument = &self.args[i];
if i < num_positional {
if names.contains_key(&argument.name) {
// todo: _originalArgumentName
return Err((
format!(
"Argument ${} was passed both by position and by name.",
argument.name
),
span,
)
.into());
}
} else if names.contains_key(&argument.name) {
named_used += 1;
} else if argument.default.is_none() {
// todo: _originalArgumentName
return Err((format!("Missing argument ${}.", argument.name), span).into());
}
}
if self.rest.is_some() {
return Ok(());
}
if num_positional > self.args.len() {
return Err((
format!(
"Only {} {}{} allowed, but {num_positional} {} passed.",
self.args.len(),
if names.is_empty() { "" } else { "positional " },
if self.args.len() == 1 {
"argument"
} else {
"arguments"
},
if num_positional == 1 { "was" } else { "were" }
),
span,
)
.into());
}
if named_used < names.len() {
let mut unknown_names = names.keys().copied().collect::<BTreeSet<_>>();
for arg in &self.args {
unknown_names.remove(&arg.name);
}
if unknown_names.len() == 1 {
return Err((
format!(
"No argument named ${}.",
unknown_names.iter().next().unwrap()
),
span,
)
.into());
}
if unknown_names.len() > 1 {
return Err((
format!(
"No arguments named {}.",
to_sentence(
unknown_names
.into_iter()
.map(|name| format!("${name}"))
.collect(),
"or"
)
),
span,
)
.into());
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub(crate) struct ArgumentInvocation {
pub positional: Vec<AstExpr>,
pub named: BTreeMap<Identifier, AstExpr>,
pub rest: Option<AstExpr>,
pub keyword_rest: Option<AstExpr>,
pub span: Span,
}
impl ArgumentInvocation {
pub fn empty(span: Span) -> Self {
Self {
positional: Vec::new(),
named: BTreeMap::new(),
rest: None,
keyword_rest: None,
span,
}
}
}
// todo: hack for builtin `call`
#[derive(Debug, Clone)]
pub(crate) enum MaybeEvaledArguments {
Invocation(ArgumentInvocation),
Evaled(ArgumentResult),
}
#[derive(Debug, Clone)]
pub(crate) struct ArgumentResult {
pub positional: Vec<Value>,
pub named: BTreeMap<Identifier, Value>,
pub separator: ListSeparator,
pub span: Span,
// todo: hack
pub touched: BTreeSet<usize>,
}
impl ArgumentResult {
/// Get argument by name
///
/// Removes the argument
pub fn get_named<T: Into<Identifier>>(&mut self, val: T) -> Option<Spanned<Value>> {
self.named.remove(&val.into()).map(|n| Spanned {
node: n,
span: self.span,
})
}
/// Get a positional argument by 0-indexed position
///
/// Replaces argument with `Value::Null` gravestone
pub fn get_positional(&mut self, idx: usize) -> Option<Spanned<Value>> {
let val = match self.positional.get_mut(idx) {
Some(v) => Some(Spanned {
node: mem::replace(v, Value::Null),
span: self.span,
}),
None => None,
};
self.touched.insert(idx);
val
}
pub fn get<T: Into<Identifier>>(&mut self, position: usize, name: T) -> Option<Spanned<Value>> {
match self.get_named(name) {
Some(v) => Some(v),
None => self.get_positional(position),
}
}
pub fn get_err(&mut self, position: usize, name: &'static str) -> SassResult<Value> {
match self.get_named(name) {
Some(v) => Ok(v.node),
None => match self.get_positional(position) {
Some(v) => Ok(v.node),
None => Err((format!("Missing argument ${}.", name), self.span()).into()),
},
}
}
pub const fn span(&self) -> Span {
self.span
}
pub fn len(&self) -> usize {
self.positional.len() + self.named.len()
}
pub fn min_args(&self, min: usize) -> SassResult<()> {
let len = self.len();
if len < min {
if min == 1 {
return Err(("At least one argument must be passed.", self.span()).into());
}
todo!("min args greater than one")
}
Ok(())
}
pub fn max_args(&self, max: usize) -> SassResult<()> {
let len = self.len();
if len > max {
let mut err = String::with_capacity(50);
#[allow(clippy::format_push_string)]
err.push_str(&format!("Only {} argument", max));
if max != 1 {
err.push('s');
}
err.push_str(" allowed, but ");
err.push_str(&len.to_string());
err.push(' ');
if len == 1 {
err.push_str("was passed.");
} else {
err.push_str("were passed.");
}
return Err((err, self.span()).into());
}
Ok(())
}
pub fn default_arg(&mut self, position: usize, name: &'static str, default: Value) -> Value {
match self.get(position, name) {
Some(val) => val.node,
None => default,
}
}
pub fn remove_positional(&mut self, position: usize) -> Option<Value> {
if self.positional.len() > position {
Some(self.positional.remove(position))
} else {
None
}
}
pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> Value {
match self.get_named(name) {
Some(val) => val.node,
None => default,
}
}
pub fn get_variadic(self) -> SassResult<Vec<Spanned<Value>>> {
if let Some((name, _)) = self.named.iter().next() {
return Err((format!("No argument named ${}.", name), self.span).into());
}
let Self {
positional,
span,
touched,
..
} = self;
// todo: complete hack, we shouldn't have the `touched` set
let args = positional
.into_iter()
.enumerate()
.filter(|(idx, _)| !touched.contains(idx))
.map(|(_, node)| Spanned { node, span })
.collect();
Ok(args)
}
}

129
src/ast/css.rs Normal file
View File

@ -0,0 +1,129 @@
use codemap::Span;
use crate::selector::ExtendedSelector;
use super::{MediaRule, Style, UnknownAtRule};
#[derive(Debug, Clone)]
pub(crate) enum CssStmt {
RuleSet {
selector: ExtendedSelector,
body: Vec<Self>,
is_group_end: bool,
},
Style(Style),
Media(MediaRule, bool),
UnknownAtRule(UnknownAtRule, bool),
Supports(SupportsRule, bool),
Comment(String, Span),
KeyframesRuleSet(KeyframesRuleSet),
/// A plain import such as `@import "foo.css";` or
/// `@import url(https://fonts.google.com/foo?bar);`
// todo: named fields, 0: url, 1: modifiers
Import(String, Option<String>),
}
impl CssStmt {
pub fn is_style_rule(&self) -> bool {
matches!(self, CssStmt::RuleSet { .. })
}
pub fn set_group_end(&mut self) {
match self {
CssStmt::Media(_, is_group_end)
| CssStmt::UnknownAtRule(_, is_group_end)
| CssStmt::Supports(_, is_group_end)
| CssStmt::RuleSet { is_group_end, .. } => *is_group_end = true,
CssStmt::Style(_)
| CssStmt::Comment(_, _)
| CssStmt::KeyframesRuleSet(_)
| CssStmt::Import(_, _) => {}
}
}
pub fn is_group_end(&self) -> bool {
match self {
CssStmt::Media(_, is_group_end)
| CssStmt::UnknownAtRule(_, is_group_end)
| CssStmt::Supports(_, is_group_end)
| CssStmt::RuleSet { is_group_end, .. } => *is_group_end,
_ => false,
}
}
pub fn is_invisible(&self) -> bool {
match self {
CssStmt::RuleSet { selector, body, .. } => {
selector.is_invisible() || body.iter().all(CssStmt::is_invisible)
}
CssStmt::Style(style) => style.value.node.is_null(),
CssStmt::Media(media_rule, ..) => media_rule.body.iter().all(CssStmt::is_invisible),
CssStmt::UnknownAtRule(..) | CssStmt::Import(..) | CssStmt::Comment(..) => false,
CssStmt::Supports(supports_rule, ..) => {
supports_rule.body.iter().all(CssStmt::is_invisible)
}
CssStmt::KeyframesRuleSet(kf) => kf.body.iter().all(CssStmt::is_invisible),
}
}
pub fn copy_without_children(&self) -> Self {
match self {
CssStmt::RuleSet {
selector,
is_group_end,
..
} => CssStmt::RuleSet {
selector: selector.clone(),
body: Vec::new(),
is_group_end: *is_group_end,
},
CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..) => unreachable!(),
CssStmt::Media(media, is_group_end) => CssStmt::Media(
MediaRule {
query: media.query.clone(),
body: Vec::new(),
},
*is_group_end,
),
CssStmt::UnknownAtRule(at_rule, is_group_end) => CssStmt::UnknownAtRule(
UnknownAtRule {
name: at_rule.name.clone(),
params: at_rule.params.clone(),
body: Vec::new(),
has_body: at_rule.has_body,
},
*is_group_end,
),
CssStmt::Supports(supports, is_group_end) => CssStmt::Supports(
SupportsRule {
params: supports.params.clone(),
body: Vec::new(),
},
*is_group_end,
),
CssStmt::KeyframesRuleSet(keyframes) => CssStmt::KeyframesRuleSet(KeyframesRuleSet {
selector: keyframes.selector.clone(),
body: Vec::new(),
}),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct KeyframesRuleSet {
pub selector: Vec<KeyframesSelector>,
pub body: Vec<CssStmt>,
}
#[derive(Debug, Clone)]
pub(crate) enum KeyframesSelector {
To,
From,
Percent(Box<str>),
}
#[derive(Debug, Clone)]
pub(crate) struct SupportsRule {
pub params: String,
pub body: Vec<CssStmt>,
}

188
src/ast/expr.rs Normal file
View File

@ -0,0 +1,188 @@
use std::iter::Iterator;
use codemap::{Span, Spanned};
use crate::{
color::Color,
common::{BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind, UnaryOp},
unit::Unit,
value::{CalculationName, Number},
};
use super::{ArgumentInvocation, AstSupportsCondition, Interpolation, InterpolationPart};
/// Represented by the `if` function
#[derive(Debug, Clone)]
pub(crate) struct Ternary(pub ArgumentInvocation);
#[derive(Debug, Clone)]
pub(crate) struct ListExpr {
pub elems: Vec<Spanned<AstExpr>>,
pub separator: ListSeparator,
pub brackets: Brackets,
}
#[derive(Debug, Clone)]
pub(crate) struct FunctionCallExpr {
pub namespace: Option<Spanned<Identifier>>,
pub name: Identifier,
pub arguments: Box<ArgumentInvocation>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct InterpolatedFunction {
pub name: Interpolation,
pub arguments: Box<ArgumentInvocation>,
pub span: Span,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct AstSassMap(pub Vec<(Spanned<AstExpr>, AstExpr)>);
#[derive(Debug, Clone)]
pub(crate) enum AstExpr {
BinaryOp {
lhs: Box<Self>,
op: BinaryOp,
rhs: Box<Self>,
allows_slash: bool,
span: Span,
},
True,
False,
Calculation {
name: CalculationName,
args: Vec<Self>,
},
Color(Box<Color>),
FunctionCall(FunctionCallExpr),
If(Box<Ternary>),
InterpolatedFunction(InterpolatedFunction),
List(ListExpr),
Map(AstSassMap),
Null,
Number {
n: Number,
unit: Unit,
},
Paren(Box<Self>),
ParentSelector,
String(StringExpr, Span),
Supports(Box<AstSupportsCondition>),
UnaryOp(UnaryOp, Box<Self>, Span),
Variable {
name: Spanned<Identifier>,
namespace: Option<Spanned<Identifier>>,
},
}
// todo: make quotes bool
// todo: track span inside
#[derive(Debug, Clone)]
pub(crate) struct StringExpr(pub Interpolation, pub QuoteKind);
impl StringExpr {
fn quote_inner_text(
text: &str,
quote: char,
buffer: &mut Interpolation,
// default=false
is_static: bool,
) {
let mut chars = text.chars().peekable();
while let Some(char) = chars.next() {
if char == '\n' || char == '\r' {
buffer.add_char('\\');
buffer.add_char('a');
if let Some(next) = chars.peek() {
if next.is_ascii_whitespace() || next.is_ascii_hexdigit() {
buffer.add_char(' ');
}
}
} else {
if char == quote
|| char == '\\'
|| (is_static && char == '#' && chars.peek() == Some(&'{'))
{
buffer.add_char('\\');
}
buffer.add_char(char);
}
}
}
fn best_quote<'a>(strings: impl Iterator<Item = &'a str>) -> char {
let mut contains_double_quote = false;
for s in strings {
for c in s.chars() {
if c == '\'' {
return '"';
}
if c == '"' {
contains_double_quote = true;
}
}
}
if contains_double_quote {
'\''
} else {
'"'
}
}
pub fn as_interpolation(self, is_static: bool) -> Interpolation {
if self.1 == QuoteKind::None {
return self.0;
}
let quote = Self::best_quote(self.0.contents.iter().filter_map(|c| match c {
InterpolationPart::Expr(..) => None,
InterpolationPart::String(text) => Some(text.as_str()),
}));
let mut buffer = Interpolation::new();
buffer.add_char(quote);
for value in self.0.contents {
match value {
InterpolationPart::Expr(e) => buffer.add_expr(e),
InterpolationPart::String(text) => {
Self::quote_inner_text(&text, quote, &mut buffer, is_static);
}
}
}
buffer.add_char(quote);
buffer
}
}
impl AstExpr {
pub fn is_variable(&self) -> bool {
matches!(self, Self::Variable { .. })
}
pub fn is_slash_operand(&self) -> bool {
match self {
Self::Number { .. } | Self::Calculation { .. } => true,
Self::BinaryOp { allows_slash, .. } => *allows_slash,
_ => false,
}
}
pub fn slash(left: Self, right: Self, span: Span) -> Self {
Self::BinaryOp {
lhs: Box::new(left),
op: BinaryOp::Div,
rhs: Box::new(right),
allows_slash: true,
span,
}
}
pub const fn span(self, span: Span) -> Spanned<Self> {
Spanned { node: self, span }
}
}

99
src/ast/interpolation.rs Normal file
View File

@ -0,0 +1,99 @@
use codemap::Spanned;
use crate::token::Token;
use super::AstExpr;
#[derive(Debug, Clone)]
pub(crate) struct Interpolation {
pub contents: Vec<InterpolationPart>,
}
impl Interpolation {
pub fn new() -> Self {
Self {
contents: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.contents.is_empty()
}
pub fn new_with_expr(e: Spanned<AstExpr>) -> Self {
Self {
contents: vec![InterpolationPart::Expr(e)],
}
}
pub fn new_plain(s: String) -> Self {
Self {
contents: vec![InterpolationPart::String(s)],
}
}
pub fn add_expr(&mut self, expr: Spanned<AstExpr>) {
self.contents.push(InterpolationPart::Expr(expr));
}
// todo: cow?
pub fn add_string(&mut self, s: String) {
match self.contents.last_mut() {
Some(InterpolationPart::String(existing)) => *existing += &s,
_ => self.contents.push(InterpolationPart::String(s)),
}
}
pub fn add_token(&mut self, tok: Token) {
match self.contents.last_mut() {
Some(InterpolationPart::String(existing)) => existing.push(tok.kind),
_ => self
.contents
.push(InterpolationPart::String(tok.kind.to_string())),
}
}
pub fn add_char(&mut self, c: char) {
match self.contents.last_mut() {
Some(InterpolationPart::String(existing)) => existing.push(c),
_ => self.contents.push(InterpolationPart::String(c.to_string())),
}
}
pub fn add_interpolation(&mut self, mut other: Self) {
self.contents.append(&mut other.contents);
}
pub fn initial_plain(&self) -> &str {
match self.contents.first() {
Some(InterpolationPart::String(s)) => s,
_ => "",
}
}
pub fn as_plain(&self) -> Option<&str> {
if self.contents.is_empty() {
Some("")
} else if self.contents.len() > 1 {
None
} else {
match self.contents.first()? {
InterpolationPart::String(s) => Some(s),
InterpolationPart::Expr(..) => None,
}
}
}
pub fn trailing_string(&self) -> &str {
match self.contents.last() {
Some(InterpolationPart::String(s)) => s,
Some(InterpolationPart::Expr(..)) | None => "",
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum InterpolationPart {
String(String),
Expr(Spanned<AstExpr>),
}

View File

@ -1,36 +1,24 @@
#![allow(dead_code)] use std::fmt::{self, Write};
use std::fmt;
use crate::{parse::Stmt, selector::Selector}; use codemap::Span;
use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser, token::Token};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct MediaRule { pub(crate) struct MediaRule {
pub super_selector: Selector, pub query: Vec<MediaQuery>,
pub query: String, pub body: Vec<CssStmt>,
pub body: Vec<Stmt>,
} }
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct MediaQuery { pub(crate) struct MediaQuery {
/// The modifier, probably either "not" or "only".
///
/// This may be `None` if no modifier is in use.
pub modifier: Option<String>, pub modifier: Option<String>,
/// The media type, for example "screen" or "print".
///
/// This may be `None`. If so, `self.features` will not be empty.
pub media_type: Option<String>, pub media_type: Option<String>,
pub conditions: Vec<String>,
/// Feature queries, including parentheses. pub conjunction: bool,
pub features: Vec<String>,
} }
impl MediaQuery { impl MediaQuery {
pub fn is_condition(&self) -> bool {
self.modifier.is_none() && self.media_type.is_none()
}
pub fn matches_all_types(&self) -> bool { pub fn matches_all_types(&self) -> bool {
self.media_type.is_none() self.media_type.is_none()
|| self || self
@ -39,16 +27,44 @@ impl MediaQuery {
.map_or(false, |v| v.to_ascii_lowercase() == "all") .map_or(false, |v| v.to_ascii_lowercase() == "all")
} }
pub fn condition(features: Vec<String>) -> Self { pub fn condition(
conditions: Vec<String>,
// default=true
conjunction: bool,
) -> Self {
Self { Self {
modifier: None, modifier: None,
media_type: None, media_type: None,
features, conditions,
conjunction,
} }
} }
pub fn media_type(
media_type: Option<String>,
modifier: Option<String>,
conditions: Option<Vec<String>>,
) -> Self {
Self {
modifier,
conjunction: true,
media_type,
conditions: conditions.unwrap_or_default(),
}
}
pub fn parse_list(list: &str, span: Span) -> SassResult<Vec<Self>> {
let toks = Lexer::new(list.chars().map(|x| Token::new(span, x)).collect());
MediaQueryParser::new(toks).parse()
}
#[allow(clippy::if_not_else)] #[allow(clippy::if_not_else)]
fn merge(&self, other: &Self) -> MediaQueryMergeResult { pub fn merge(&self, other: &Self) -> MediaQueryMergeResult {
if !self.conjunction || !other.conjunction {
return MediaQueryMergeResult::Unrepresentable;
}
let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase()); let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase());
let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase()); let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase());
let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase()); let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase());
@ -56,33 +72,34 @@ impl MediaQuery {
if this_type.is_none() && other_type.is_none() { if this_type.is_none() && other_type.is_none() {
return MediaQueryMergeResult::Success(Self::condition( return MediaQueryMergeResult::Success(Self::condition(
self.features self.conditions
.iter() .iter()
.chain(&other.features) .chain(&other.conditions)
.cloned() .cloned()
.collect(), .collect(),
true,
)); ));
} }
let modifier; let modifier;
let media_type; let media_type;
let features; let conditions;
if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) { if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) {
if this_modifier == other_modifier { if this_modifier == other_modifier {
let negative_features = if this_modifier.as_deref() == Some("not") { let negative_conditions = if this_modifier.as_deref() == Some("not") {
&self.features &self.conditions
} else { } else {
&other.features &other.conditions
}; };
let positive_features = if this_modifier.as_deref() == Some("not") { let positive_conditions = if this_modifier.as_deref() == Some("not") {
&other.features &other.conditions
} else { } else {
&self.features &self.conditions
}; };
// If the negative features are a subset of the positive features, the // If the negative conditions are a subset of the positive conditions, the
// query is empty. For example, `not screen and (color)` has no // query is empty. For example, `not screen and (color)` has no
// intersection with `screen and (color) and (grid)`. // intersection with `screen and (color) and (grid)`.
// //
@ -90,9 +107,9 @@ impl MediaQuery {
// (grid)`, because it means `not (screen and (color))` and so it allows // (grid)`, because it means `not (screen and (color))` and so it allows
// a screen with no color but with a grid. // a screen with no color but with a grid.
if negative_features if negative_conditions
.iter() .iter()
.all(|feat| positive_features.contains(feat)) .all(|feat| positive_conditions.contains(feat))
{ {
return MediaQueryMergeResult::Empty; return MediaQueryMergeResult::Empty;
} }
@ -105,11 +122,11 @@ impl MediaQuery {
if this_modifier.as_deref() == Some("not") { if this_modifier.as_deref() == Some("not") {
modifier = &other_modifier; modifier = &other_modifier;
media_type = &other_type; media_type = &other_type;
features = other.features.clone(); conditions = other.conditions.clone();
} else { } else {
modifier = &this_modifier; modifier = &this_modifier;
media_type = &this_type; media_type = &this_type;
features = self.features.clone(); conditions = self.conditions.clone();
} }
} else if this_modifier.as_deref() == Some("not") { } else if this_modifier.as_deref() == Some("not") {
debug_assert_eq!(other_modifier.as_deref(), Some("not")); debug_assert_eq!(other_modifier.as_deref(), Some("not"));
@ -119,27 +136,27 @@ impl MediaQuery {
return MediaQueryMergeResult::Unrepresentable; return MediaQueryMergeResult::Unrepresentable;
} }
let more_features = if self.features.len() > other.features.len() { let more_conditions = if self.conditions.len() > other.conditions.len() {
&self.features &self.conditions
} else { } else {
&other.features &other.conditions
}; };
let fewer_features = if self.features.len() > other.features.len() { let fewer_conditions = if self.conditions.len() > other.conditions.len() {
&other.features &other.conditions
} else { } else {
&self.features &self.conditions
}; };
// If one set of features is a superset of the other, use those features // If one set of conditions is a superset of the other, use those conditions
// because they're strictly narrower. // because they're strictly narrower.
if fewer_features if fewer_conditions
.iter() .iter()
.all(|feat| more_features.contains(feat)) .all(|feat| more_conditions.contains(feat))
{ {
modifier = &this_modifier; // "not" modifier = &this_modifier;
media_type = &this_type; media_type = &this_type;
features = more_features.clone(); conditions = more_conditions.clone();
} else { } else {
// Otherwise, there's no way to represent the intersection. // Otherwise, there's no way to represent the intersection.
return MediaQueryMergeResult::Unrepresentable; return MediaQueryMergeResult::Unrepresentable;
@ -155,19 +172,19 @@ impl MediaQuery {
&other_type &other_type
}; };
features = self conditions = self
.features .conditions
.iter() .iter()
.chain(&other.features) .chain(&other.conditions)
.cloned() .cloned()
.collect(); .collect();
} else if other.matches_all_types() { } else if other.matches_all_types() {
modifier = &this_modifier; modifier = &this_modifier;
media_type = &this_type; media_type = &this_type;
features = self conditions = self
.features .conditions
.iter() .iter()
.chain(&other.features) .chain(&other.conditions)
.cloned() .cloned()
.collect(); .collect();
} else if this_type != other_type { } else if this_type != other_type {
@ -180,10 +197,10 @@ impl MediaQuery {
} }
media_type = &this_type; media_type = &this_type;
features = self conditions = self
.features .conditions
.iter() .iter()
.chain(&other.features) .chain(&other.conditions)
.cloned() .cloned()
.collect(); .collect();
} }
@ -199,7 +216,8 @@ impl MediaQuery {
} else { } else {
other.modifier.clone() other.modifier.clone()
}, },
features, conditions,
conjunction: true,
}) })
} }
} }
@ -208,19 +226,22 @@ impl fmt::Display for MediaQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(modifier) = &self.modifier { if let Some(modifier) = &self.modifier {
f.write_str(modifier)?; f.write_str(modifier)?;
f.write_char(' ')?;
} }
if let Some(media_type) = &self.media_type { if let Some(media_type) = &self.media_type {
f.write_str(media_type)?; f.write_str(media_type)?;
if !&self.features.is_empty() { if !&self.conditions.is_empty() {
f.write_str(" and ")?; f.write_str(" and ")?;
} }
} }
f.write_str(&self.features.join(" and "))
f.write_str(&self.conditions.join(" and "))
} }
} }
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
enum MediaQueryMergeResult { pub(crate) enum MediaQueryMergeResult {
Empty, Empty,
Unrepresentable, Unrepresentable,
Success(MediaQuery), Success(MediaQuery),

32
src/ast/mixin.rs Normal file
View File

@ -0,0 +1,32 @@
use std::fmt;
use crate::{
ast::ArgumentResult,
error::SassResult,
evaluate::{Environment, Visitor},
};
pub(crate) type BuiltinMixin = fn(ArgumentResult, &mut Visitor) -> SassResult<()>;
pub(crate) use crate::ast::AstMixin as UserDefinedMixin;
#[derive(Clone)]
pub(crate) enum Mixin {
UserDefined(UserDefinedMixin, Environment),
Builtin(BuiltinMixin),
}
impl fmt::Debug for Mixin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UserDefined(u, ..) => f
.debug_struct("AstMixin")
.field("name", &u.name)
.field("args", &u.args)
.field("body", &u.body)
.field("has_content", &u.has_content)
.finish(),
Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(),
}
}
}

19
src/ast/mod.rs Normal file
View File

@ -0,0 +1,19 @@
pub(crate) use args::*;
pub(crate) use css::*;
pub(crate) use expr::*;
pub(crate) use interpolation::*;
pub(crate) use media::*;
pub(crate) use mixin::*;
pub(crate) use stmt::*;
pub(crate) use style::*;
pub(crate) use unknown::*;
mod args;
mod css;
mod expr;
mod interpolation;
mod media;
mod mixin;
mod stmt;
mod style;
mod unknown;

568
src/ast/stmt.rs Normal file
View File

@ -0,0 +1,568 @@
use std::{
cell::RefCell,
collections::{BTreeMap, HashSet},
path::PathBuf,
sync::Arc,
};
use codemap::{Span, Spanned};
use crate::{
ast::{ArgumentDeclaration, ArgumentInvocation, AstExpr, CssStmt},
ast::{Interpolation, MediaQuery},
common::Identifier,
utils::{BaseMapView, LimitedMapView, MapView, UnprefixedMapView},
value::Value,
};
#[derive(Debug, Clone)]
#[allow(unused)]
pub(crate) struct AstSilentComment {
pub text: String,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstPlainCssImport {
pub url: Interpolation,
pub modifiers: Option<Interpolation>,
#[allow(unused)]
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstSassImport {
pub url: String,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstIf {
pub if_clauses: Vec<AstIfClause>,
pub else_clause: Option<Vec<AstStmt>>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstIfClause {
pub condition: AstExpr,
pub body: Vec<AstStmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstFor {
pub variable: Spanned<Identifier>,
pub from: Spanned<AstExpr>,
pub to: Spanned<AstExpr>,
pub is_exclusive: bool,
pub body: Vec<AstStmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstReturn {
pub val: AstExpr,
#[allow(unused)]
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstRuleSet {
pub selector: Interpolation,
pub body: Vec<AstStmt>,
pub selector_span: Span,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstStyle {
pub name: Interpolation,
pub value: Option<Spanned<AstExpr>>,
pub body: Vec<AstStmt>,
pub span: Span,
}
impl AstStyle {
pub fn is_custom_property(&self) -> bool {
self.name.initial_plain().starts_with("--")
}
}
#[derive(Debug, Clone)]
pub(crate) struct AstEach {
pub variables: Vec<Identifier>,
pub list: AstExpr,
pub body: Vec<AstStmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstMedia {
pub query: Interpolation,
pub body: Vec<AstStmt>,
pub span: Span,
}
pub(crate) type CssMediaQuery = MediaQuery;
#[derive(Debug, Clone)]
pub(crate) struct AstWhile {
pub condition: AstExpr,
pub body: Vec<AstStmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstVariableDecl {
pub namespace: Option<Spanned<Identifier>>,
pub name: Identifier,
pub value: AstExpr,
pub is_guarded: bool,
pub is_global: bool,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstFunctionDecl {
pub name: Spanned<Identifier>,
pub arguments: ArgumentDeclaration,
pub children: Vec<AstStmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstDebugRule {
pub value: AstExpr,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstWarn {
pub value: AstExpr,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstErrorRule {
pub value: AstExpr,
pub span: Span,
}
impl PartialEq for AstFunctionDecl {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for AstFunctionDecl {}
#[derive(Debug, Clone)]
pub(crate) struct AstLoudComment {
pub text: Interpolation,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstMixin {
pub name: Identifier,
pub args: ArgumentDeclaration,
pub body: Vec<AstStmt>,
/// Whether the mixin contains a `@content` rule.
pub has_content: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct AstContentRule {
pub args: ArgumentInvocation,
}
#[derive(Debug, Clone)]
pub(crate) struct AstContentBlock {
pub args: ArgumentDeclaration,
pub body: Vec<AstStmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct AstInclude {
pub namespace: Option<Spanned<Identifier>>,
pub name: Spanned<Identifier>,
pub args: ArgumentInvocation,
pub content: Option<AstContentBlock>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstUnknownAtRule {
pub name: Interpolation,
pub value: Option<Interpolation>,
pub children: Option<Vec<AstStmt>>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstExtendRule {
pub value: Interpolation,
pub is_optional: bool,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AstAtRootRule {
pub children: Vec<AstStmt>,
pub query: Option<Interpolation>,
#[allow(unused)]
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct AtRootQuery {
pub include: bool,
pub names: HashSet<String>,
pub all: bool,
pub rule: bool,
}
impl AtRootQuery {
pub fn new(include: bool, names: HashSet<String>) -> Self {
let all = names.contains("all");
let rule = names.contains("rule");
Self {
include,
names,
all,
rule,
}
}
pub fn excludes_name(&self, name: &str) -> bool {
(self.all || self.names.contains(name)) != self.include
}
pub fn excludes_style_rules(&self) -> bool {
(self.all || self.rule) != self.include
}
pub fn excludes(&self, stmt: &CssStmt) -> bool {
if self.all {
return !self.include;
}
match stmt {
CssStmt::RuleSet { .. } => self.excludes_style_rules(),
CssStmt::Media(..) => self.excludes_name("media"),
CssStmt::Supports(..) => self.excludes_name("supports"),
CssStmt::UnknownAtRule(rule, ..) => self.excludes_name(&rule.name.to_ascii_lowercase()),
_ => false,
}
}
}
impl Default for AtRootQuery {
fn default() -> Self {
Self {
include: false,
names: HashSet::new(),
all: false,
rule: true,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct AstImportRule {
pub imports: Vec<AstImport>,
}
#[derive(Debug, Clone)]
pub(crate) enum AstImport {
Plain(AstPlainCssImport),
Sass(AstSassImport),
}
impl AstImport {
pub fn is_dynamic(&self) -> bool {
matches!(self, AstImport::Sass(..))
}
}
#[derive(Debug, Clone)]
pub(crate) struct AstUseRule {
pub url: PathBuf,
pub namespace: Option<String>,
pub configuration: Vec<ConfiguredVariable>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) struct ConfiguredVariable {
pub name: Spanned<Identifier>,
pub expr: Spanned<AstExpr>,
pub is_guarded: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct Configuration {
pub values: Arc<dyn MapView<Value = ConfiguredValue>>,
#[allow(unused)]
pub original_config: Option<Arc<RefCell<Self>>>,
pub span: Option<Span>,
}
impl Configuration {
pub fn through_forward(
config: Arc<RefCell<Self>>,
forward: &AstForwardRule,
) -> Arc<RefCell<Self>> {
if (*config).borrow().is_empty() {
return Arc::new(RefCell::new(Configuration::empty()));
}
let mut new_values = Arc::clone(&(*config).borrow().values);
// Only allow variables that are visible through the `@forward` to be
// configured. These views support [Map.remove] so we can mark when a
// configuration variable is used by removing it even when the underlying
// map is wrapped.
if let Some(prefix) = &forward.prefix {
new_values = Arc::new(UnprefixedMapView(new_values, prefix.clone()));
}
if let Some(shown_variables) = &forward.shown_variables {
new_values = Arc::new(LimitedMapView::safelist(new_values, shown_variables));
} else if let Some(hidden_variables) = &forward.hidden_variables {
new_values = Arc::new(LimitedMapView::blocklist(new_values, hidden_variables));
}
Arc::new(RefCell::new(Self::with_values(
config,
Arc::clone(&new_values),
)))
}
fn with_values(
config: Arc<RefCell<Self>>,
values: Arc<dyn MapView<Value = ConfiguredValue>>,
) -> Self {
Self {
values,
original_config: Some(config),
span: None,
}
}
pub fn first(&self) -> Option<Spanned<Identifier>> {
let name = *self.values.keys().get(0)?;
let value = self.values.get(name)?;
Some(Spanned {
node: name,
span: value.configuration_span?,
})
}
pub fn remove(&mut self, name: Identifier) -> Option<ConfiguredValue> {
self.values.remove(name)
}
pub fn is_implicit(&self) -> bool {
self.span.is_none()
}
pub fn implicit(values: BTreeMap<Identifier, ConfiguredValue>) -> Self {
Self {
values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))),
original_config: None,
span: None,
}
}
pub fn explicit(values: BTreeMap<Identifier, ConfiguredValue>, span: Span) -> Self {
Self {
values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))),
original_config: None,
span: Some(span),
}
}
pub fn empty() -> Self {
Self {
values: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
original_config: None,
span: None,
}
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[allow(unused)]
pub fn original_config(config: Arc<RefCell<Configuration>>) -> Arc<RefCell<Configuration>> {
match (*config).borrow().original_config.as_ref() {
Some(v) => Arc::clone(v),
None => Arc::clone(&config),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ConfiguredValue {
pub value: Value,
pub configuration_span: Option<Span>,
}
impl ConfiguredValue {
pub fn explicit(value: Value, configuration_span: Span) -> Self {
Self {
value,
configuration_span: Some(configuration_span),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct AstForwardRule {
pub url: PathBuf,
pub shown_mixins_and_functions: Option<HashSet<Identifier>>,
pub shown_variables: Option<HashSet<Identifier>>,
pub hidden_mixins_and_functions: Option<HashSet<Identifier>>,
pub hidden_variables: Option<HashSet<Identifier>>,
pub prefix: Option<String>,
pub configuration: Vec<ConfiguredVariable>,
pub span: Span,
}
impl AstForwardRule {
pub fn new(
url: PathBuf,
prefix: Option<String>,
configuration: Option<Vec<ConfiguredVariable>>,
span: Span,
) -> Self {
Self {
url,
shown_mixins_and_functions: None,
shown_variables: None,
hidden_mixins_and_functions: None,
hidden_variables: None,
prefix,
configuration: configuration.unwrap_or_default(),
span,
}
}
pub fn show(
url: PathBuf,
shown_mixins_and_functions: HashSet<Identifier>,
shown_variables: HashSet<Identifier>,
prefix: Option<String>,
configuration: Option<Vec<ConfiguredVariable>>,
span: Span,
) -> Self {
Self {
url,
shown_mixins_and_functions: Some(shown_mixins_and_functions),
shown_variables: Some(shown_variables),
hidden_mixins_and_functions: None,
hidden_variables: None,
prefix,
configuration: configuration.unwrap_or_default(),
span,
}
}
pub fn hide(
url: PathBuf,
hidden_mixins_and_functions: HashSet<Identifier>,
hidden_variables: HashSet<Identifier>,
prefix: Option<String>,
configuration: Option<Vec<ConfiguredVariable>>,
span: Span,
) -> Self {
Self {
url,
shown_mixins_and_functions: None,
shown_variables: None,
hidden_mixins_and_functions: Some(hidden_mixins_and_functions),
hidden_variables: Some(hidden_variables),
prefix,
configuration: configuration.unwrap_or_default(),
span,
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum AstSupportsCondition {
Anything {
contents: Interpolation,
},
Declaration {
name: AstExpr,
value: AstExpr,
},
Function {
name: Interpolation,
args: Interpolation,
},
Interpolation(AstExpr),
Negation(Box<Self>),
Operation {
left: Box<Self>,
operator: Option<String>,
right: Box<Self>,
},
}
#[derive(Debug, Clone)]
pub(crate) struct AstSupportsRule {
pub condition: AstSupportsCondition,
pub children: Vec<AstStmt>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub(crate) enum AstStmt {
If(AstIf),
For(AstFor),
Return(AstReturn),
RuleSet(AstRuleSet),
Style(AstStyle),
Each(AstEach),
Media(AstMedia),
Include(AstInclude),
While(AstWhile),
VariableDecl(AstVariableDecl),
LoudComment(AstLoudComment),
SilentComment(AstSilentComment),
FunctionDecl(AstFunctionDecl),
Mixin(AstMixin),
ContentRule(AstContentRule),
Warn(AstWarn),
UnknownAtRule(AstUnknownAtRule),
ErrorRule(AstErrorRule),
Extend(AstExtendRule),
AtRootRule(AstAtRootRule),
Debug(AstDebugRule),
ImportRule(AstImportRule),
Use(AstUseRule),
Forward(AstForwardRule),
Supports(AstSupportsRule),
}
#[derive(Debug, Clone)]
pub(crate) struct StyleSheet {
pub body: Vec<AstStmt>,
pub url: PathBuf,
pub is_plain_css: bool,
pub uses: Vec<AstUseRule>,
pub forwards: Vec<AstForwardRule>,
}
impl StyleSheet {
pub fn new(is_plain_css: bool, url: PathBuf) -> Self {
Self {
body: Vec::new(),
url,
is_plain_css,
uses: Vec::new(),
forwards: Vec::new(),
}
}
}

11
src/ast/style.rs Normal file
View File

@ -0,0 +1,11 @@
use codemap::Spanned;
use crate::{interner::InternedString, value::Value};
/// A style: `color: red`
#[derive(Clone, Debug)]
pub(crate) struct Style {
pub property: InternedString,
pub value: Box<Spanned<Value>>,
pub declared_as_custom_property: bool,
}

View File

@ -1,12 +1,12 @@
use crate::{parse::Stmt, selector::Selector}; use crate::ast::CssStmt;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) struct UnknownAtRule { pub(crate) struct UnknownAtRule {
pub name: String, pub name: String,
pub super_selector: Selector, // pub super_selector: Selector,
pub params: String, pub params: String,
pub body: Vec<Stmt>, pub body: Vec<CssStmt>,
/// Whether or not this @-rule was declared with curly /// Whether or not this @-rule was declared with curly
/// braces. A body may not necessarily have contents /// braces. A body may not necessarily have contents

View File

@ -1,38 +0,0 @@
use std::hash::{Hash, Hasher};
use codemap::Span;
use crate::{args::FuncArgs, Token};
#[derive(Debug, Clone)]
pub(crate) struct Function {
pub args: FuncArgs,
pub body: Vec<Token>,
pub declared_at_root: bool,
pos: Span,
}
impl Hash for Function {
fn hash<H: Hasher>(&self, state: &mut H) {
self.pos.hash(state);
}
}
impl PartialEq for Function {
fn eq(&self, other: &Self) -> bool {
self.pos == other.pos
}
}
impl Eq for Function {}
impl Function {
pub fn new(args: FuncArgs, body: Vec<Token>, declared_at_root: bool, pos: Span) -> Self {
Function {
args,
body,
declared_at_root,
pos,
}
}
}

View File

@ -1,24 +0,0 @@
use crate::parse::Stmt;
#[derive(Debug, Clone)]
pub(crate) struct Keyframes {
/// `@keyframes` can contain a browser prefix,
/// e.g. `@-webkit-keyframes { ... }`, and therefore
/// we cannot be certain of the name of the at-rule
pub rule: String,
pub name: String,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct KeyframesRuleSet {
pub selector: Vec<KeyframesSelector>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone)]
pub(crate) enum KeyframesSelector {
To,
From,
Percent(Box<str>),
}

View File

@ -1,106 +0,0 @@
use std::convert::TryFrom;
use codemap::Spanned;
use crate::{common::unvendor, error::SassError};
#[derive(Debug, PartialEq, Eq)]
pub enum AtRuleKind {
// Sass specific @rules
/// Loads mixins, functions, and variables from other Sass
/// stylesheets, and combines CSS from multiple stylesheets together
Use,
/// Loads a Sass stylesheet and makes its mixins, functions,
/// and variables available when your stylesheet is loaded
/// with the `@use` rule
Forward,
/// Extends the CSS at-rule to load styles, mixins, functions,
/// and variables from other stylesheets
///
/// The definition inside `grass` however differs in that
/// the @import rule refers to a plain css import
/// e.g. `@import url(foo);`
Import,
Mixin,
Content,
Include,
/// Defines custom functions that can be used in SassScript
/// expressions
Function,
Return,
/// Allows selectors to inherit styles from one another
Extend,
/// Puts styles within it at the root of the CSS document
AtRoot,
/// Causes compilation to fail with an error message
Error,
/// Prints a warning without stopping compilation entirely
Warn,
/// Prints a message for debugging purposes
Debug,
If,
Each,
For,
While,
// CSS @rules
/// Defines the character set used by the style sheet
Charset,
/// A conditional group rule that will apply its content if the
/// browser meets the criteria of the given condition
Supports,
/// Describes the aspect of intermediate steps in a CSS animation sequence
Keyframes,
Media,
/// An unknown at-rule
Unknown(String),
}
impl TryFrom<&Spanned<String>> for AtRuleKind {
type Error = Box<SassError>;
fn try_from(c: &Spanned<String>) -> Result<Self, Box<SassError>> {
match c.node.as_str() {
"use" => return Ok(Self::Use),
"forward" => return Ok(Self::Forward),
"import" => return Ok(Self::Import),
"mixin" => return Ok(Self::Mixin),
"include" => return Ok(Self::Include),
"function" => return Ok(Self::Function),
"return" => return Ok(Self::Return),
"extend" => return Ok(Self::Extend),
"at-root" => return Ok(Self::AtRoot),
"error" => return Ok(Self::Error),
"warn" => return Ok(Self::Warn),
"debug" => return Ok(Self::Debug),
"if" => return Ok(Self::If),
"each" => return Ok(Self::Each),
"for" => return Ok(Self::For),
"while" => return Ok(Self::While),
"charset" => return Ok(Self::Charset),
"supports" => return Ok(Self::Supports),
"content" => return Ok(Self::Content),
"media" => return Ok(Self::Media),
"else" => return Err(("This at-rule is not allowed here.", c.span).into()),
"" => return Err(("Expected identifier.", c.span).into()),
_ => {}
}
Ok(match unvendor(&c.node) {
"keyframes" => Self::Keyframes,
_ => Self::Unknown(c.node.clone()),
})
}
}

View File

@ -1,90 +0,0 @@
use std::fmt;
use crate::{
args::{CallArgs, FuncArgs},
error::SassResult,
parse::{Parser, Stmt},
Token,
};
pub(crate) type BuiltinMixin = fn(CallArgs, &mut Parser) -> SassResult<Vec<Stmt>>;
#[derive(Clone)]
pub(crate) enum Mixin {
UserDefined(UserDefinedMixin),
Builtin(BuiltinMixin),
}
impl fmt::Debug for Mixin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UserDefined(u) => f
.debug_struct("UserDefinedMixin")
.field("args", &u.args)
.field("body", &u.body)
.field("accepts_content_block", &u.accepts_content_block)
.field("declared_at_root", &u.declared_at_root)
.finish(),
Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(),
}
}
}
impl Mixin {
pub fn new_user_defined(
args: FuncArgs,
body: Vec<Token>,
accepts_content_block: bool,
declared_at_root: bool,
) -> Self {
Mixin::UserDefined(UserDefinedMixin::new(
args,
body,
accepts_content_block,
declared_at_root,
))
}
}
#[derive(Debug, Clone)]
pub(crate) struct UserDefinedMixin {
pub args: FuncArgs,
pub body: Vec<Token>,
pub accepts_content_block: bool,
pub declared_at_root: bool,
}
impl UserDefinedMixin {
pub fn new(
args: FuncArgs,
body: Vec<Token>,
accepts_content_block: bool,
declared_at_root: bool,
) -> Self {
Self {
args,
body,
accepts_content_block,
declared_at_root,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Content {
/// The literal block, serialized as a list of tokens
pub content: Option<Vec<Token>>,
/// Optional args, e.g. `@content(a, b, c);`
pub content_args: Option<FuncArgs>,
/// The number of scopes at the use of `@include`
///
/// This is used to "reset" back to the state of the `@include`
/// without actually cloning the scope or putting it in an `Rc`
pub scope_len: usize,
/// Whether or not the mixin this `@content` block is inside of was
/// declared in the global scope
pub declared_at_root: bool,
}

View File

@ -1,12 +0,0 @@
pub(crate) use function::Function;
pub(crate) use kind::AtRuleKind;
pub(crate) use supports::SupportsRule;
pub(crate) use unknown::UnknownAtRule;
mod function;
pub mod keyframes;
mod kind;
pub mod media;
pub mod mixin;
mod supports;
mod unknown;

View File

@ -1,7 +0,0 @@
use crate::parse::Stmt;
#[derive(Debug, Clone)]
pub(crate) struct SupportsRule {
pub params: String,
pub body: Vec<Stmt>,
}

View File

@ -1,115 +1,28 @@
use super::{Builtin, GlobalFunctionMap}; use std::collections::{BTreeMap, BTreeSet};
use codemap::Spanned; use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber};
use num_traits::One;
use crate::{ use super::rgb::{function_string, parse_channels, percentage_or_unitless, ParsedChannels};
args::CallArgs,
color::Color,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn hsl_3_args(
if args.is_empty() { name: &'static str,
return Err(("Missing argument $channels.", args.span()).into()); mut args: ArgumentResult,
} visitor: &mut Visitor,
) -> SassResult<Value> {
let span = args.span();
let len = args.len();
if len == 1 {
let mut channels = match args.get_err(0, "channels")? {
Value::List(v, ..) => v,
v if v.is_special_function() => vec![v],
_ => return Err(("Missing argument $channels.", args.span()).into()),
};
if channels.len() > 3 {
return Err((
format!(
"Only 3 elements allowed, but {} were passed.",
channels.len()
),
args.span(),
)
.into());
}
if channels.iter().any(Value::is_special_function) {
let channel_sep = if channels.len() < 3 {
ListSeparator::Space
} else {
ListSeparator::Comma
};
return Ok(Value::String(
format!(
"{}({})",
name,
Value::List(channels, channel_sep, Brackets::None)
.to_css_string(args.span(), false)?
),
QuoteKind::None,
));
}
let lightness = match channels.pop() {
Some(Value::Dimension(Some(n), ..)) => n / Number::from(100),
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) => {
return Err((
format!("$lightness: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
None => return Err(("Missing element $lightness.", args.span()).into()),
};
let saturation = match channels.pop() {
Some(Value::Dimension(Some(n), ..)) => n / Number::from(100),
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) => {
return Err((
format!("$saturation: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
None => return Err(("Missing element $saturation.", args.span()).into()),
};
let hue = match channels.pop() {
Some(Value::Dimension(Some(n), ..)) => n,
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) => {
return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
None => return Err(("Missing element $hue.", args.span()).into()),
};
Ok(Value::Color(Box::new(Color::from_hsla(
hue,
saturation,
lightness,
Number::one(),
))))
} else {
let hue = args.get_err(0, "hue")?; let hue = args.get_err(0, "hue")?;
let saturation = args.get_err(1, "saturation")?; let saturation = args.get_err(1, "saturation")?;
let lightness = args.get_err(2, "lightness")?; let lightness = args.get_err(2, "lightness")?;
let alpha = args.default_arg( let alpha = args.default_arg(
3, 3,
"alpha", "alpha",
Value::Dimension(Some(Number::one()), Unit::None, true), Value::Dimension(SassNumber {
)?; num: (Number::one()),
unit: Unit::None,
as_slash: None,
}),
);
if [&hue, &saturation, &lightness, &alpha] if [&hue, &saturation, &lightness, &alpha]
.iter() .iter()
@ -121,7 +34,7 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
"{}({})", "{}({})",
name, name,
Value::List( Value::List(
if len == 4 { if args.len() == 4 {
vec![hue, saturation, lightness, alpha] vec![hue, saturation, lightness, alpha]
} else { } else {
vec![hue, saturation, lightness] vec![hue, saturation, lightness]
@ -135,85 +48,89 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
)); ));
} }
let hue = match hue { let hue = hue.assert_number_with_name("hue", span)?;
Value::Dimension(Some(n), ..) => n, let saturation = saturation.assert_number_with_name("saturation", span)?;
Value::Dimension(None, ..) => todo!(), let lightness = lightness.assert_number_with_name("lightness", span)?;
v => { let alpha = percentage_or_unitless(
return Err(( &alpha.assert_number_with_name("alpha", span)?,
format!("$hue: {} is not a number.", v.inspect(args.span())?), 1.0,
args.span(), "alpha",
) span,
.into()) visitor,
} )?;
};
let saturation = match saturation { Ok(Value::Color(Box::new(Color::from_hsla_fn(
Value::Dimension(Some(n), ..) => n / Number::from(100), Number(hue.num().rem_euclid(360.0)),
Value::Dimension(None, ..) => todo!(), saturation.num() / Number::from(100),
v => { lightness.num() / Number::from(100),
return Err(( Number(alpha),
format!(
"$saturation: {} is not a number.",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
};
let lightness = match lightness {
Value::Dimension(Some(n), ..) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!(
"$lightness: {} is not a number.",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
};
let alpha = match alpha {
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
"$alpha: Expected {} to have no units or \"%\".",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
v => {
return Err((
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
Ok(Value::Color(Box::new(Color::from_hsla(
hue, saturation, lightness, alpha,
)))) ))))
} }
fn inner_hsl(
name: &'static str,
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(4)?;
let span = args.span();
let len = args.len();
if len == 1 || len == 0 {
match parse_channels(
name,
&["hue", "saturation", "lightness"],
args.get_err(0, "channels")?,
visitor,
args.span(),
)? {
ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)),
ParsedChannels::List(list) => {
let args = ArgumentResult {
positional: list,
named: BTreeMap::new(),
separator: ListSeparator::Comma,
span: args.span(),
touched: BTreeSet::new(),
};
hsl_3_args(name, args, visitor)
}
}
} else if len == 2 {
let hue = args.get_err(0, "hue")?;
let saturation = args.get_err(1, "saturation")?;
if hue.is_var() || saturation.is_var() {
return Ok(Value::String(
function_string(name, &[hue, saturation], visitor, span)?,
QuoteKind::None,
));
} else {
return Err(("Missing argument $lightness.", args.span()).into());
}
} else {
return hsl_3_args(name, args, visitor);
}
} }
pub(crate) fn hsl(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
inner_hsl("hsl", args, parser) inner_hsl("hsl", args, visitor)
} }
pub(crate) fn hsla(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
inner_hsl("hsla", args, parser) inner_hsl("hsla", args, visitor)
} }
pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.hue()), Unit::Deg, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.hue()),
unit: Unit::Deg,
as_slash: None,
})),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -222,10 +139,14 @@ pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
} }
pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn saturation(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.saturation()), Unit::Percent, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.saturation()),
unit: Unit::Percent,
as_slash: None,
})),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -234,10 +155,14 @@ pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult<
} }
} }
pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn lightness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.lightness()), Unit::Percent, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: c.lightness(),
unit: Unit::Percent,
as_slash: None,
})),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -246,36 +171,20 @@ pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
} }
} }
pub(crate) fn adjust_hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = args
Value::Color(c) => c, .get_err(0, "color")?
v => { .assert_color_with_name("color", args.span())?;
return Err(( let degrees = args
format!("$color: {} is not a color.", v.inspect(args.span())?), .get_err(1, "degrees")?
args.span(), .assert_number_with_name("degrees", args.span())?
) .num();
.into())
}
};
let degrees = match args.get_err(1, "degrees")? {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!(
"$degrees: {} is not a number.",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
};
Ok(Value::Color(Box::new(color.adjust_hue(degrees)))) Ok(Value::Color(Box::new(color.adjust_hue(degrees))))
} }
fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -287,24 +196,16 @@ fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
.into()) .into())
} }
}; };
let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), let amount = args
Value::Dimension(None, ..) => todo!(), .get_err(1, "amount")?
v => { .assert_number_with_name("amount", args.span())?;
return Err(( let amount = bound!(args, "amount", amount.num(), amount.unit, 0, 100) / Number(100.0);
format!(
"$amount: {} is not a number.",
v.to_css_string(args.span(), false)?
),
args.span(),
)
.into())
}
};
Ok(Value::Color(Box::new(color.lighten(amount)))) Ok(Value::Color(Box::new(color.lighten(amount))))
} }
fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -317,8 +218,12 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
v => { v => {
return Err(( return Err((
format!( format!(
@ -333,22 +238,29 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
Ok(Value::Color(Box::new(color.darken(amount)))) Ok(Value::Color(Box::new(color.darken(amount))))
} }
fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
if args.len() == 1 { if args.len() == 1 {
let amount = args
.get_err(0, "amount")?
.assert_number_with_name("amount", args.span())?;
return Ok(Value::String( return Ok(Value::String(
format!( format!(
"saturate({})", "saturate({})",
args.get_err(0, "amount")? serialize_number(&amount, &Options::default(), args.span())?,
.to_css_string(args.span(), false)?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
} }
let amount = match args.get_err(1, "amount")? { let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
v => { v => {
return Err(( return Err((
format!( format!(
@ -362,11 +274,16 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
}; };
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
Value::Dimension(Some(n), u, _) => { Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => {
// todo: this branch should be superfluous/incorrect
return Ok(Value::String( return Ok(Value::String(
format!("saturate({}{})", n.inspect(), u), format!("saturate({}{})", n.inspect(), u),
QuoteKind::None, QuoteKind::None,
)) ));
} }
v => { v => {
return Err(( return Err((
@ -379,7 +296,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
Ok(Value::Color(Box::new(color.saturate(amount)))) Ok(Value::Color(Box::new(color.saturate(amount))))
} }
fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn desaturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -392,13 +309,17 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
v => { v => {
return Err(( return Err((
format!( format!(
"$amount: {} is not a number.", "$amount: {} is not a number.",
v.to_css_string(args.span(), parser.options.is_compressed())? v.to_css_string(args.span(), visitor.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -408,11 +329,15 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
Ok(Value::Color(Box::new(color.desaturate(amount)))) Ok(Value::Color(Box::new(color.desaturate(amount))))
} }
pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn grayscale(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
Value::Dimension(Some(n), u, _) => { Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => {
return Ok(Value::String( return Ok(Value::String(
format!("grayscale({}{})", n.inspect(), u), format!("grayscale({}{})", n.inspect(), u),
QuoteKind::None, QuoteKind::None,
@ -429,7 +354,7 @@ pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
Ok(Value::Color(Box::new(color.desaturate(Number::one())))) Ok(Value::Color(Box::new(color.desaturate(Number::one()))))
} }
pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn complement(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -444,24 +369,28 @@ pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser) -> SassResult<
Ok(Value::Color(Box::new(color.complement()))) Ok(Value::Color(Box::new(color.complement())))
} }
pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn invert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let weight = match args.get(1, "weight") { let weight = match args.get(1, "weight") {
Some(Err(e)) => return Err(e), Some(Spanned {
Some(Ok(Spanned { node: Value::Dimension(SassNumber { num: n, .. }),
node: Value::Dimension(Some(n), u, _),
.. ..
})) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)), }) if n.is_nan() => todo!(),
Some(Ok(Spanned { Some(Spanned {
node: Value::Dimension(None, ..), node:
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}),
.. ..
})) => todo!(), }) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
None => None, None => None,
Some(Ok(v)) => { Some(v) => {
return Err(( return Err((
format!( format!(
"$weight: {} is not a number.", "$weight: {} is not a number.",
v.to_css_string(args.span(), parser.options.is_compressed())? v.to_css_string(args.span(), visitor.options.is_compressed())?
), ),
args.span(), args.span(),
) )
@ -472,7 +401,11 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
Value::Color(c) => Ok(Value::Color(Box::new( Value::Color(c) => Ok(Value::Color(Box::new(
c.invert(weight.unwrap_or_else(Number::one)), c.invert(weight.unwrap_or_else(Number::one)),
))), ))),
Value::Dimension(Some(n), u, _) => { Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => {
if weight.is_some() { if weight.is_some() {
return Err(( return Err((
"Only one argument may be passed to the plain-CSS invert() function.", "Only one argument may be passed to the plain-CSS invert() function.",
@ -485,9 +418,6 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
QuoteKind::None, QuoteKind::None,
)) ))
} }
Value::Dimension(None, u, _) => {
Ok(Value::String(format!("invert(NaN{})", u), QuoteKind::None))
}
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),

View File

@ -1,15 +1,8 @@
use num_traits::One; use crate::builtin::builtin_imports::*;
use crate::{ use super::rgb::{parse_channels, ParsedChannels};
args::CallArgs,
color::Color,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
@ -26,39 +19,36 @@ pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
let blackness = let blackness =
Number::from(1) - (color.red().max(color.green()).max(color.blue()) / Number::from(255)); Number::from(1) - (color.red().max(color.green()).max(color.blue()) / Number::from(255));
Ok(Value::Dimension(Some(blackness * 100), Unit::Percent, true)) Ok(Value::Dimension(SassNumber {
num: (blackness * 100),
unit: Unit::Percent,
as_slash: None,
}))
} }
pub(crate) fn whiteness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = args
Value::Color(c) => c, .get_err(0, "color")?
v => { .assert_color_with_name("color", args.span())?;
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let whiteness = color.red().min(color.green()).min(color.blue()) / Number::from(255); let whiteness = color.red().min(color.green()).min(color.blue()) / Number::from(255);
Ok(Value::Dimension(Some(whiteness * 100), Unit::Percent, true)) Ok(Value::Dimension(SassNumber {
num: (whiteness * 100),
unit: Unit::Percent,
as_slash: None,
}))
} }
pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(4)?; let span = args.span();
if args.is_empty() {
return Err(("Missing argument $channels.", args.span()).into());
}
let hue = match args.get(0, "hue") { let hue = match args.get(0, "hue") {
Some(Ok(v)) => match v.node { Some(v) => match v.node {
Value::Dimension(Some(n), ..) => n, Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber { num: n, .. }) => n,
v => { v => {
return Err(( return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?), format!("$hue: {} is not a number.", v.inspect(args.span())?),
@ -67,57 +57,28 @@ pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
.into()) .into())
} }
}, },
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $hue.", args.span()).into()), None => return Err(("Missing element $hue.", args.span()).into()),
}; };
let whiteness = match args.get(1, "whiteness") { let whiteness = args
Some(Ok(v)) => match v.node { .get_err(1, "whiteness")?
Value::Dimension(Some(n), Unit::Percent, ..) => n, .assert_number_with_name("whiteness", span)?;
v @ Value::Dimension(Some(..), ..) => { whiteness.assert_unit(&Unit::Percent, "whiteness", span)?;
return Err((
format!(
"$whiteness: Expected {} to have unit \"%\".",
v.inspect(args.span())?
),
args.span(),
)
.into())
}
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$whiteness: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $whiteness.", args.span()).into()),
};
let blackness = match args.get(2, "blackness") { let blackness = args
Some(Ok(v)) => match v.node { .get_err(2, "blackness")?
Value::Dimension(Some(n), ..) => n, .assert_number_with_name("blackness", span)?;
Value::Dimension(None, ..) => todo!(), blackness.assert_unit(&Unit::Percent, "blackness", span)?;
v => {
return Err((
format!("$blackness: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $blackness.", args.span()).into()),
};
let alpha = match args.get(3, "alpha") { let alpha = match args.get(3, "alpha") {
Some(Ok(v)) => match v.node { Some(v) => match v.node {
Value::Dimension(Some(n), Unit::Percent, ..) => n / Number::from(100), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(Some(n), ..) => n, Value::Dimension(SassNumber {
Value::Dimension(None, ..) => todo!(), num: n,
unit: Unit::Percent,
..
}) => n / Number::from(100),
Value::Dimension(SassNumber { num: n, .. }) => n,
v => { v => {
return Err(( return Err((
format!("$alpha: {} is not a number.", v.inspect(args.span())?), format!("$alpha: {} is not a number.", v.inspect(args.span())?),
@ -126,11 +87,44 @@ pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
.into()) .into())
} }
}, },
Some(Err(e)) => return Err(e),
None => Number::one(), None => Number::one(),
}; };
Ok(Value::Color(Box::new(Color::from_hwb( Ok(Value::Color(Box::new(Color::from_hwb(
hue, whiteness, blackness, alpha, hue,
whiteness.num,
blackness.num,
alpha,
)))) ))))
} }
pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(4)?;
if args.len() == 0 || args.len() == 1 {
match parse_channels(
"hwb",
&["hue", "whiteness", "blackness"],
args.get_err(0, "channels")?,
visitor,
args.span(),
)? {
ParsedChannels::String(s) => {
Err((format!("Expected numeric channels, got {}", s), args.span()).into())
}
ParsedChannels::List(list) => {
let args = ArgumentResult {
positional: list,
named: BTreeMap::new(),
separator: ListSeparator::Comma,
span: args.span(),
touched: BTreeSet::new(),
};
hwb_inner(args, visitor)
}
}
} else {
hwb_inner(args, visitor)
}
}

View File

@ -1,4 +1,4 @@
use super::{Builtin, GlobalFunctionMap}; use super::GlobalFunctionMap;
pub mod hsl; pub mod hsl;
pub mod hwb; pub mod hwb;

View File

@ -1,9 +1,4 @@
use super::{Builtin, GlobalFunctionMap}; use crate::builtin::builtin_imports::*;
use crate::{
args::CallArgs, common::QuoteKind, error::SassResult, parse::Parser, unit::Unit, value::Number,
value::Value,
};
/// Check if `s` matches the regex `^[a-zA-Z]+\s*=` /// Check if `s` matches the regex `^[a-zA-Z]+\s*=`
fn is_ms_filter(s: &str) -> bool { fn is_ms_filter(s: &str) -> bool {
@ -35,10 +30,14 @@ mod test {
} }
} }
pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn alpha(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
if args.len() <= 1 { if args.len() <= 1 {
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.alpha()),
unit: Unit::None,
as_slash: None,
})),
Value::String(s, QuoteKind::None) if is_ms_filter(&s) => { Value::String(s, QuoteKind::None) if is_ms_filter(&s) => {
Ok(Value::String(format!("alpha({})", s), QuoteKind::None)) Ok(Value::String(format!("alpha({})", s), QuoteKind::None))
} }
@ -69,15 +68,23 @@ pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
} }
} }
pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn opacity(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(Some(num), unit, _) => Ok(Value::String( Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.alpha()),
unit: Unit::None,
as_slash: None,
})),
Value::Dimension(SassNumber {
num,
unit,
as_slash: _,
}) => Ok(Value::String(
format!("opacity({}{})", num.inspect(), unit), format!("opacity({}{})", num.inspect(), unit),
QuoteKind::None, QuoteKind::None,
)), )),
Value::Dimension(None, ..) => todo!(),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -86,8 +93,7 @@ pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
} }
} }
// todo: unify `opacify` and `fade_in` fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -99,21 +105,16 @@ fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
.into()) .into())
} }
}; };
let amount = match args.get_err(1, "amount")? { let amount = args
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1), .get_err(1, "amount")?
Value::Dimension(None, ..) => todo!(), .assert_number_with_name("amount", args.span())?;
v => {
return Err(( let amount = bound!(args, "amount", amount.num(), amount.unit(), 0, 1);
format!("$amount: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
Ok(Value::Color(Box::new(color.fade_in(amount)))) Ok(Value::Color(Box::new(color.fade_in(amount))))
} }
fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -126,61 +127,12 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber {
v => { num: n,
return Err(( unit: u,
format!("$amount: {} is not a number.", v.inspect(args.span())?), as_slash: _,
args.span(), }) => bound!(args, "amount", n, u, 0, 1),
)
.into())
}
};
Ok(Value::Color(Box::new(color.fade_in(amount))))
}
// todo: unify with `fade_out`
fn transparentize(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(2)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
v => {
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$amount: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
Ok(Value::Color(Box::new(color.fade_out(amount))))
}
fn fade_out(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(2)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
v => {
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let amount = match args.get_err(1, "amount")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
Value::Dimension(None, ..) => todo!(),
v => { v => {
return Err(( return Err((
format!("$amount: {} is not a number.", v.inspect(args.span())?), format!("$amount: {} is not a number.", v.inspect(args.span())?),
@ -196,7 +148,7 @@ pub(crate) fn declare(f: &mut GlobalFunctionMap) {
f.insert("alpha", Builtin::new(alpha)); f.insert("alpha", Builtin::new(alpha));
f.insert("opacity", Builtin::new(opacity)); f.insert("opacity", Builtin::new(opacity));
f.insert("opacify", Builtin::new(opacify)); f.insert("opacify", Builtin::new(opacify));
f.insert("fade-in", Builtin::new(fade_in)); f.insert("fade-in", Builtin::new(opacify));
f.insert("transparentize", Builtin::new(transparentize)); f.insert("transparentize", Builtin::new(transparentize));
f.insert("fade-out", Builtin::new(fade_out)); f.insert("fade-out", Builtin::new(transparentize));
} }

View File

@ -1,22 +1,12 @@
use super::{Builtin, GlobalFunctionMap}; use crate::builtin::builtin_imports::*;
use num_traits::{One, Signed, Zero};
use crate::{
args::CallArgs,
color::Color,
common::QuoteKind,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
macro_rules! opt_rgba { macro_rules! opt_rgba {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? { let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(Some(n), u, _) => Some(bound!($args, $arg, n, u, $low, $high)), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => Some(bound!($args, $arg, n, u, $low, $high)),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -31,11 +21,11 @@ macro_rules! opt_rgba {
macro_rules! opt_hsl { macro_rules! opt_hsl {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? { let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(Some(n), u, _) => { Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100)) Value::Dimension(SassNumber {
} num: n, unit: u, ..
Value::Dimension(None, ..) => todo!(), }) => Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100)),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -48,8 +38,8 @@ macro_rules! opt_hsl {
}; };
} }
pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
if args.positional_arg(1).is_some() { if args.get_positional(1).is_some() {
return Err(( return Err((
"Only one positional argument is allowed. All other arguments must be passed by name.", "Only one positional argument is allowed. All other arguments must be passed by name.",
args.span(), args.span(),
@ -82,9 +72,9 @@ pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
)))); ))));
} }
let hue = match args.default_named_arg("hue", Value::Null)? { let hue = match args.default_named_arg("hue", Value::Null) {
Value::Dimension(Some(n), ..) => Some(n), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber { num: n, .. }) => Some(n),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -116,7 +106,7 @@ pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
})) }))
} }
pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
v => { v => {
@ -142,9 +132,9 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
)))); ))));
} }
let hue = match args.default_named_arg("hue", Value::Null)? { let hue = match args.default_named_arg("hue", Value::Null) {
Value::Dimension(Some(n), ..) => Some(n), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(None, ..) => todo!(), Value::Dimension(SassNumber { num: n, .. }) => Some(n),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -179,12 +169,12 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
// todo: refactor into rgb and hsl? // todo: refactor into rgb and hsl?
pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number { pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
if by.is_zero() { if by.is_zero() {
return val; return val;
} }
val.clone() + (if by.is_positive() { max - val } else { val }) * by val + (if by.is_positive() { max - val } else { val }) * by
} }
let span = args.span(); let span = args.span();
@ -201,12 +191,14 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
macro_rules! opt_scale_arg { macro_rules! opt_scale_arg {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? { let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(Some(n), Unit::Percent, _) => { Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)) Value::Dimension(SassNumber {
} num: n,
Value::Dimension(None, ..) => todo!(), unit: Unit::Percent,
v @ Value::Dimension(..) => { ..
}) => Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)),
v @ Value::Dimension { .. } => {
return Err(( return Err((
format!( format!(
"${}: Expected {} to have unit \"%\".", "${}: Expected {} to have unit \"%\".",
@ -293,7 +285,7 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
})) }))
} }
pub(crate) fn ie_hex_str(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,

View File

@ -1,368 +1,365 @@
use super::{Builtin, GlobalFunctionMap}; use crate::{builtin::builtin_imports::*, serializer::inspect_number, value::fuzzy_round};
use num_traits::One; pub(crate) fn function_string(
name: &'static str,
args: &[Value],
visitor: &mut Visitor,
span: Span,
) -> SassResult<String> {
let args = args
.iter()
.map(|arg| arg.to_css_string(span, visitor.options.is_compressed()))
.collect::<SassResult<Vec<_>>>()?
.join(", ");
use crate::{ Ok(format!("{}({})", name, args))
args::CallArgs,
color::Color,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
/// name: Either `rgb` or `rgba` depending on the caller
// todo: refactor into smaller functions
#[allow(clippy::cognitive_complexity)]
fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
if args.is_empty() {
return Err(("Missing argument $channels.", args.span()).into());
} }
let len = args.len(); fn inner_rgb_2_arg(
name: &'static str,
if len == 1 { mut args: ArgumentResult,
let mut channels = match args.get_err(0, "channels")? { visitor: &mut Visitor,
Value::List(v, ..) => v, ) -> SassResult<Value> {
v if v.is_special_function() => vec![v], // rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
_ => return Err(("Missing argument $channels.", args.span()).into()), // and functions are parsed after variable substitution.
};
if channels.len() > 3 {
return Err((
format!(
"Only 3 elements allowed, but {} were passed.",
channels.len()
),
args.span(),
)
.into());
}
if channels.iter().any(Value::is_special_function) {
let channel_sep = if channels.len() < 3 {
ListSeparator::Space
} else {
ListSeparator::Comma
};
return Ok(Value::String(
format!(
"{}({})",
name,
Value::List(channels, channel_sep, Brackets::None)
.to_css_string(args.span(), false)?
),
QuoteKind::None,
));
}
let blue = match channels.pop() {
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255)
}
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => {
let green = channels.pop().unwrap();
let red = channels.pop().unwrap();
return Ok(Value::String(
format!(
"{}({}, {}, {})",
name,
red.to_css_string(args.span(), parser.options.is_compressed())?,
green.to_css_string(args.span(), parser.options.is_compressed())?,
v.to_css_string(args.span(), parser.options.is_compressed())?
),
QuoteKind::None,
));
}
Some(v) => {
return Err((
format!("$blue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
None => return Err(("Missing element $blue.", args.span()).into()),
};
let green = match channels.pop() {
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255)
}
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => {
let string = match channels.pop() {
Some(red) => format!(
"{}({}, {}, {})",
name,
red.to_css_string(args.span(), parser.options.is_compressed())?,
v.to_css_string(args.span(), parser.options.is_compressed())?,
blue.to_string(parser.options.is_compressed())
),
None => format!(
"{}({} {})",
name,
v.to_css_string(args.span(), parser.options.is_compressed())?,
blue.to_string(parser.options.is_compressed())
),
};
return Ok(Value::String(string, QuoteKind::None));
}
Some(v) => {
return Err((
format!("$green: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
None => return Err(("Missing element $green.", args.span()).into()),
};
let red = match channels.pop() {
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255)
}
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => {
return Ok(Value::String(
format!(
"{}({}, {}, {})",
name,
v.to_css_string(args.span(), parser.options.is_compressed())?,
green.to_string(parser.options.is_compressed()),
blue.to_string(parser.options.is_compressed())
),
QuoteKind::None,
));
}
Some(v) => {
return Err((
format!("$red: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
None => return Err(("Missing element $red.", args.span()).into()),
};
let color = Color::from_rgba(red, green, blue, Number::one());
Ok(Value::Color(Box::new(color)))
} else if len == 2 {
let color = args.get_err(0, "color")?; let color = args.get_err(0, "color")?;
let alpha = args.get_err(1, "alpha")?; let alpha = args.get_err(1, "alpha")?;
if color.is_special_function() || (alpha.is_special_function() && !color.is_color()) { let is_compressed = visitor.options.is_compressed();
if color.is_var() {
return Ok(Value::String( return Ok(Value::String(
format!( function_string(name, &[color, alpha], visitor, args.span())?,
"{}({})",
name,
Value::List(vec![color, alpha], ListSeparator::Comma, Brackets::None)
.to_css_string(args.span(), false)?
),
QuoteKind::None, QuoteKind::None,
)); ));
} } else if alpha.is_var() {
match &color {
let color = match color { Value::Color(color) => {
Value::Color(c) => c,
v => {
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
if alpha.is_special_function() {
return Ok(Value::String( return Ok(Value::String(
format!( format!(
"{}({}, {}, {}, {})", "{}({}, {}, {}, {})",
name, name,
color.red().to_string(false), color.red().to_string(is_compressed),
color.green().to_string(false), color.green().to_string(is_compressed),
color.blue().to_string(false), color.blue().to_string(is_compressed),
alpha.to_css_string(args.span(), false)?, alpha.to_css_string(args.span(), is_compressed)?
),
QuoteKind::None,
));
}
_ => {
return Ok(Value::String(
function_string(name, &[color, alpha], visitor, args.span())?,
QuoteKind::None,
))
}
}
} else if alpha.is_special_function() {
let color = color.assert_color_with_name("color", args.span())?;
return Ok(Value::String(
format!(
"{}({}, {}, {}, {})",
name,
color.red().to_string(is_compressed),
color.green().to_string(is_compressed),
color.blue().to_string(is_compressed),
alpha.to_css_string(args.span(), is_compressed)?
), ),
QuoteKind::None, QuoteKind::None,
)); ));
} }
let alpha = match alpha { let color = color.assert_color_with_name("color", args.span())?;
Value::Dimension(Some(n), Unit::None, _) => n, let alpha = alpha.assert_number_with_name("alpha", args.span())?;
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100), Ok(Value::Color(Box::new(color.with_alpha(Number(
Value::Dimension(None, ..) => todo!(), percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?,
v @ Value::Dimension(..) => { )))))
return Err((
format!(
"$alpha: Expected {} to have no units or \"%\".",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
} }
v => {
return Err(( fn inner_rgb_3_arg(
format!("$alpha: {} is not a number.", v.inspect(args.span())?), name: &'static str,
args.span(), mut args: ArgumentResult,
) visitor: &mut Visitor,
.into()) ) -> SassResult<Value> {
} let alpha = if args.len() > 3 {
}; args.get(3, "alpha")
Ok(Value::Color(Box::new(color.with_alpha(alpha))))
} else { } else {
None
};
let red = args.get_err(0, "red")?; let red = args.get_err(0, "red")?;
let green = args.get_err(1, "green")?; let green = args.get_err(1, "green")?;
let blue = args.get_err(2, "blue")?; let blue = args.get_err(2, "blue")?;
let alpha = args.default_arg(
3,
"alpha",
Value::Dimension(Some(Number::one()), Unit::None, true),
)?;
if [&red, &green, &blue, &alpha] if red.is_special_function()
.iter() || green.is_special_function()
.copied() || blue.is_special_function()
.any(Value::is_special_function) || alpha
.as_ref()
.map(|alpha| alpha.node.is_special_function())
.unwrap_or(false)
{ {
return Ok(Value::String( let fn_string = if alpha.is_some() {
format!( function_string(
"{}({})",
name, name,
Value::List( &[red, green, blue, alpha.unwrap().node],
if len == 4 { visitor,
vec![red, green, blue, alpha] args.span(),
)?
} else { } else {
vec![red, green, blue] function_string(name, &[red, green, blue], visitor, args.span())?
}, };
ListSeparator::Comma,
Brackets::None return Ok(Value::String(fn_string, QuoteKind::None));
)
.to_css_string(args.span(), false)?
),
QuoteKind::None,
));
} }
let red = match red { let span = args.span();
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => { let red = red.assert_number_with_name("red", span)?;
(n / Number::from(100)) * Number::from(255) let green = green.assert_number_with_name("green", span)?;
} let blue = blue.assert_number_with_name("blue", span)?;
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { Ok(Value::Color(Box::new(Color::from_rgba_fn(
return Err(( Number(fuzzy_round(percentage_or_unitless(
format!( &red, 255.0, "red", span, visitor,
"$red: Expected {} to have no units or \"%\".", )?)),
v.to_css_string(args.span(), parser.options.is_compressed())? Number(fuzzy_round(percentage_or_unitless(
&green, 255.0, "green", span, visitor,
)?)),
Number(fuzzy_round(percentage_or_unitless(
&blue, 255.0, "blue", span, visitor,
)?)),
Number(
alpha
.map(|alpha| {
percentage_or_unitless(
&alpha.node.assert_number_with_name("alpha", span)?,
1.0,
"alpha",
span,
visitor,
)
})
.transpose()?
.unwrap_or(1.0),
), ),
args.span(),
)
.into())
}
v => {
return Err((
format!("$red: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let green = match green {
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
"$green: Expected {} to have no units or \"%\".",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
v => {
return Err((
format!("$green: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let blue = match blue {
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
"$blue: Expected {} to have no units or \"%\".",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
v => {
return Err((
format!("$blue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let alpha = match alpha {
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
"$alpha: Expected {} to have no units or \"%\".",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
args.span(),
)
.into())
}
v => {
return Err((
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
Ok(Value::Color(Box::new(Color::from_rgba(
red, green, blue, alpha,
)))) ))))
} }
pub(crate) fn percentage_or_unitless(
number: &SassNumber,
max: f64,
name: &str,
span: Span,
visitor: &mut Visitor,
) -> SassResult<f64> {
let value = if number.unit == Unit::None {
number.num
} else if number.unit == Unit::Percent {
(number.num * Number(max)) / Number(100.0)
} else {
return Err((
format!(
"${name}: Expected {} to have no units or \"%\".",
inspect_number(number, visitor.options, span)?
),
span,
)
.into());
};
Ok(value.clamp(0.0, max).0)
} }
pub(crate) fn rgb(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { #[derive(Debug, Clone)]
inner_rgb("rgb", args, parser) pub(crate) enum ParsedChannels {
String(String),
List(Vec<Value>),
} }
pub(crate) fn rgba(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn is_var_slash(value: &Value) -> bool {
inner_rgb("rgba", args, parser) match value {
Value::String(text, QuoteKind::Quoted) => {
text.to_ascii_lowercase().starts_with("var(") && text.contains('/')
}
_ => false,
}
} }
pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn parse_channels(
name: &'static str,
arg_names: &[&'static str],
mut channels: Value,
visitor: &mut Visitor,
span: Span,
) -> SassResult<ParsedChannels> {
if channels.is_var() {
let fn_string = function_string(name, &[channels], visitor, span)?;
return Ok(ParsedChannels::String(fn_string));
}
let original_channels = channels.clone();
let mut alpha_from_slash_list = None;
if channels.separator() == ListSeparator::Slash {
let list = channels.clone().as_list();
if list.len() != 2 {
return Err((
format!(
"Only 2 slash-separated elements allowed, but {} {} passed.",
list.len(),
if list.len() == 1 { "was" } else { "were" }
),
span,
)
.into());
}
channels = list[0].clone();
let inner_alpha_from_slash_list = list[1].clone();
if !alpha_from_slash_list
.as_ref()
.map(Value::is_special_function)
.unwrap_or(false)
{
inner_alpha_from_slash_list
.clone()
.assert_number_with_name("alpha", span)?;
}
alpha_from_slash_list = Some(inner_alpha_from_slash_list);
if list[0].is_var() {
let fn_string = function_string(name, &[original_channels], visitor, span)?;
return Ok(ParsedChannels::String(fn_string));
}
}
let is_comma_separated = channels.separator() == ListSeparator::Comma;
let is_bracketed = matches!(channels, Value::List(_, _, Brackets::Bracketed));
if is_comma_separated || is_bracketed {
let mut err_buffer = "$channels must be".to_owned();
if is_bracketed {
err_buffer.push_str(" an unbracketed");
}
if is_comma_separated {
if is_bracketed {
err_buffer.push(',');
} else {
err_buffer.push_str(" a");
}
err_buffer.push_str(" space-separated");
}
err_buffer.push_str(" list.");
return Err((err_buffer, span).into());
}
let mut list = channels.clone().as_list();
if list.len() > 3 {
return Err((
format!("Only 3 elements allowed, but {} were passed.", list.len()),
span,
)
.into());
} else if list.len() < 3 {
if list.iter().any(Value::is_var)
|| (!list.is_empty() && is_var_slash(list.last().unwrap()))
{
let fn_string = function_string(name, &[original_channels], visitor, span)?;
return Ok(ParsedChannels::String(fn_string));
} else {
let argument = arg_names[list.len()];
return Err((format!("Missing element ${argument}."), span).into());
}
}
if let Some(alpha_from_slash_list) = alpha_from_slash_list {
list.push(alpha_from_slash_list);
return Ok(ParsedChannels::List(list));
}
#[allow(clippy::collapsible_match)]
match &list[2] {
Value::Dimension(SassNumber { as_slash, .. }) => match as_slash {
Some(slash) => Ok(ParsedChannels::List(vec![
list[0].clone(),
list[1].clone(),
// todo: superfluous clones
Value::Dimension(slash.0.clone()),
Value::Dimension(slash.1.clone()),
])),
None => Ok(ParsedChannels::List(list)),
},
Value::String(text, QuoteKind::None) if text.contains('/') => {
let fn_string = function_string(name, &[channels], visitor, span)?;
Ok(ParsedChannels::String(fn_string))
}
_ => Ok(ParsedChannels::List(list)),
}
}
/// name: Either `rgb` or `rgba` depending on the caller
fn inner_rgb(
name: &'static str,
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(4)?;
match args.len() {
0 | 1 => {
match parse_channels(
name,
&["red", "green", "blue"],
args.get_err(0, "channels")?,
visitor,
args.span(),
)? {
ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)),
ParsedChannels::List(list) => {
let args = ArgumentResult {
positional: list,
named: BTreeMap::new(),
separator: ListSeparator::Comma,
span: args.span(),
touched: BTreeSet::new(),
};
inner_rgb_3_arg(name, args, visitor)
}
}
}
2 => inner_rgb_2_arg(name, args, visitor),
_ => inner_rgb_3_arg(name, args, visitor),
}
}
pub(crate) fn rgb(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
inner_rgb("rgb", args, visitor)
}
pub(crate) fn rgba(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
inner_rgb("rgba", args, visitor)
}
pub(crate) fn red(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.red()), Unit::None, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.red()),
unit: Unit::None,
as_slash: None,
})),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -371,10 +368,14 @@ pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
} }
pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn green(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.green()), Unit::None, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.green()),
unit: Unit::None,
as_slash: None,
})),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -383,10 +384,14 @@ pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
} }
} }
pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn blue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.blue()), Unit::None, true)), Value::Color(c) => Ok(Value::Dimension(SassNumber {
num: (c.blue()),
unit: Unit::None,
as_slash: None,
})),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -395,7 +400,7 @@ pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
} }
pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let color1 = match args.get_err(0, "color1")? { let color1 = match args.get_err(0, "color1")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -422,15 +427,23 @@ pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
let weight = match args.default_arg( let weight = match args.default_arg(
2, 2,
"weight", "weight",
Value::Dimension(Some(Number::from(50)), Unit::None, true), Value::Dimension(SassNumber {
)? { num: (Number::from(50)),
Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), unit: Unit::None,
Value::Dimension(None, ..) => todo!(), as_slash: None,
}),
) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
v => { v => {
return Err(( return Err((
format!( format!(
"$weight: {} is not a number.", "$weight: {} is not a number.",
v.to_css_string(args.span(), parser.options.is_compressed())? v.to_css_string(args.span(), visitor.options.is_compressed())?
), ),
args.span(), args.span(),
) )

View File

@ -1,33 +1,24 @@
use super::{Builtin, GlobalFunctionMap}; use crate::builtin::builtin_imports::*;
use num_traits::{Signed, ToPrimitive, Zero}; pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
use crate::{
args::CallArgs,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
pub(crate) fn length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::Dimension( Ok(Value::Dimension(SassNumber {
Some(Number::from(args.get_err(0, "list")?.as_list().len())), num: (Number::from(args.get_err(0, "list")?.as_list().len())),
Unit::None, unit: Unit::None,
true, as_slash: None,
)) }))
} }
pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let mut list = args.get_err(0, "list")?.as_list(); let mut list = args.get_err(0, "list")?.as_list();
let (n, unit) = match args.get_err(1, "n")? { let (n, unit) = match args.get_err(1, "n")? {
Value::Dimension(Some(num), unit, ..) => (num, unit), Value::Dimension(SassNumber {
Value::Dimension(None, u, ..) => { num: n, unit: u, ..
}) if n.is_nan() => {
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into()) return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
} }
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
v => { v => {
return Err(( return Err((
format!("$n: {} is not a number.", v.inspect(args.span())?), format!("$n: {} is not a number.", v.inspect(args.span())?),
@ -54,18 +45,16 @@ pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
.into()); .into());
} }
if n.is_decimal() {
return Err((format!("$n: {} is not an int.", n.inspect()), args.span()).into());
}
Ok(list.remove(if n.is_positive() { Ok(list.remove(if n.is_positive() {
n.to_integer().to_usize().unwrap_or(std::usize::MAX) - 1 let index = n.assert_int_with_name("n", args.span())? - 1;
debug_assert!(index > -1);
index as usize
} else { } else {
list.len() - n.abs().to_integer().to_usize().unwrap_or(std::usize::MAX) list.len() - n.abs().assert_int_with_name("n", args.span())? as usize
})) }))
} }
pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn list_separator(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::String( Ok(Value::String(
match args.get_err(0, "list")? { match args.get_err(0, "list")? {
@ -78,12 +67,12 @@ pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser) -> SassRes
)) ))
} }
pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? { let (mut list, sep, brackets) = match args.get_err(0, "list")? {
Value::List(v, sep, b) => (v, sep, b), Value::List(v, sep, b) => (v, sep, b),
Value::ArgList(v) => ( Value::ArgList(v) => (
v.into_iter().map(|val| val.node).collect(), v.elems.into_iter().collect(),
ListSeparator::Comma, ListSeparator::Comma,
Brackets::None, Brackets::None,
), ),
@ -91,10 +80,12 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
v => (vec![v], ListSeparator::Space, Brackets::None), v => (vec![v], ListSeparator::Space, Brackets::None),
}; };
let (n, unit) = match args.get_err(1, "n")? { let (n, unit) = match args.get_err(1, "n")? {
Value::Dimension(Some(num), unit, ..) => (num, unit), Value::Dimension(SassNumber {
Value::Dimension(None, u, ..) => { num: n, unit: u, ..
}) if n.is_nan() => {
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into()) return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
} }
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
v => { v => {
return Err(( return Err((
format!("$n: {} is not a number.", v.inspect(args.span())?), format!("$n: {} is not a number.", v.inspect(args.span())?),
@ -130,15 +121,15 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
let val = args.get_err(2, "value")?; let val = args.get_err(2, "value")?;
if n.is_positive() { if n.is_positive() {
list[n.to_integer().to_usize().unwrap_or(std::usize::MAX) - 1] = val; list[n.assert_int_with_name("n", args.span())? as usize - 1] = val;
} else { } else {
list[len - n.abs().to_integer().to_usize().unwrap_or(std::usize::MAX)] = val; list[len - n.abs().assert_int_with_name("n", args.span())? as usize] = val;
} }
Ok(Value::List(list, sep, brackets)) Ok(Value::List(list, sep, brackets))
} }
pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? { let (mut list, sep, brackets) = match args.get_err(0, "list")? {
Value::List(v, sep, b) => (v, sep, b), Value::List(v, sep, b) => (v, sep, b),
@ -149,14 +140,15 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
2, 2,
"separator", "separator",
Value::String("auto".to_owned(), QuoteKind::None), Value::String("auto".to_owned(), QuoteKind::None),
)? { ) {
Value::String(s, ..) => match s.as_str() { Value::String(s, ..) => match s.as_str() {
"auto" => sep, "auto" => sep,
"comma" => ListSeparator::Comma, "comma" => ListSeparator::Comma,
"space" => ListSeparator::Space, "space" => ListSeparator::Space,
"slash" => ListSeparator::Slash,
_ => { _ => {
return Err(( return Err((
"$separator: Must be \"space\", \"comma\", or \"auto\".", "$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
args.span(), args.span(),
) )
.into()) .into())
@ -176,7 +168,7 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
Ok(Value::List(list, sep, brackets)) Ok(Value::List(list, sep, brackets))
} }
pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(4)?; args.max_args(4)?;
let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? { let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
Value::List(v, sep, brackets) => (v, sep, brackets), Value::List(v, sep, brackets) => (v, sep, brackets),
@ -192,7 +184,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
2, 2,
"separator", "separator",
Value::String("auto".to_owned(), QuoteKind::None), Value::String("auto".to_owned(), QuoteKind::None),
)? { ) {
Value::String(s, ..) => match s.as_str() { Value::String(s, ..) => match s.as_str() {
"auto" => { "auto" => {
if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) { if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) {
@ -203,9 +195,10 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
"comma" => ListSeparator::Comma, "comma" => ListSeparator::Comma,
"space" => ListSeparator::Space, "space" => ListSeparator::Space,
"slash" => ListSeparator::Slash,
_ => { _ => {
return Err(( return Err((
"$separator: Must be \"space\", \"comma\", or \"auto\".", "$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
args.span(), args.span(),
) )
.into()) .into())
@ -224,7 +217,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
3, 3,
"bracketed", "bracketed",
Value::String("auto".to_owned(), QuoteKind::None), Value::String("auto".to_owned(), QuoteKind::None),
)? { ) {
Value::String(s, ..) => match s.as_str() { Value::String(s, ..) => match s.as_str() {
"auto" => brackets, "auto" => brackets,
_ => Brackets::Bracketed, _ => Brackets::Bracketed,
@ -243,7 +236,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
Ok(Value::List(list1, sep, brackets)) Ok(Value::List(list1, sep, brackets))
} }
pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn is_bracketed(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::bool(match args.get_err(0, "list")? { Ok(Value::bool(match args.get_err(0, "list")? {
Value::List(.., brackets) => match brackets { Value::List(.., brackets) => match brackets {
@ -254,7 +247,7 @@ pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResul
})) }))
} }
pub(crate) fn index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let list = args.get_err(0, "list")?.as_list(); let list = args.get_err(0, "list")?.as_list();
let value = args.get_err(1, "value")?; let value = args.get_err(1, "value")?;
@ -262,10 +255,14 @@ pub(crate) fn index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
Some(v) => Number::from(v + 1), Some(v) => Number::from(v + 1),
None => return Ok(Value::Null), None => return Ok(Value::Null),
}; };
Ok(Value::Dimension(Some(index), Unit::None, true)) Ok(Value::Dimension(SassNumber {
num: (index),
unit: Unit::None,
as_slash: None,
}))
} }
pub(crate) fn zip(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn zip(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let lists = args let lists = args
.get_variadic()? .get_variadic()?
.into_iter() .into_iter()

View File

@ -1,26 +1,6 @@
macro_rules! bound { macro_rules! bound {
($args:ident, $name:literal, $arg:ident, $unit:ident, $low:literal, $high:literal) => { ($args:ident, $name:literal, $arg:expr, $unit:expr, $low:literal, $high:literal) => {
if $arg > Number::from($high) || $arg < Number::from($low) { if !($arg <= Number::from($high) && $arg >= Number::from($low)) {
return Err((
format!(
"${}: Expected {}{} to be within {}{} and {}{}.",
$name,
$arg.inspect(),
$unit,
$low,
$unit,
$high,
$unit,
),
$args.span(),
)
.into());
} else {
$arg
}
};
($args:ident, $name:literal, $arg:ident, $unit:path, $low:literal, $high:literal) => {
if $arg > Number::from($high) || $arg < Number::from($low) {
return Err(( return Err((
format!( format!(
"${}: Expected {}{} to be within {}{} and {}{}.", "${}: Expected {}{} to be within {}{} and {}{}.",

View File

@ -1,14 +1,6 @@
use super::{Builtin, GlobalFunctionMap}; use crate::builtin::builtin_imports::*;
use crate::{ pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args::CallArgs,
common::{Brackets, ListSeparator},
error::SassResult,
parse::Parser,
value::{SassMap, Value},
};
pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let key = args.get_err(1, "key")?; let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
@ -26,7 +18,7 @@ pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
Ok(map.get(&key).unwrap_or(Value::Null)) Ok(map.get(&key).unwrap_or(Value::Null))
} }
pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let key = args.get_err(1, "key")?; let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
@ -44,7 +36,7 @@ pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser) -> SassResult
Ok(Value::bool(map.get(&key).is_some())) Ok(Value::bool(map.get(&key).is_some()))
} }
pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
Value::Map(m) => m, Value::Map(m) => m,
@ -65,7 +57,7 @@ pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser) -> SassResult<Va
)) ))
} }
pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
Value::Map(m) => m, Value::Map(m) => m,
@ -86,7 +78,7 @@ pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser) -> SassResult<
)) ))
} }
pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
if args.len() == 1 { if args.len() == 1 {
return Err(("Expected $args to contain a key.", args.span()).into()); return Err(("Expected $args to contain a key.", args.span()).into());
} }
@ -150,10 +142,10 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
while let Some((key, queued_map)) = map_queue.pop() { while let Some((key, queued_map)) = map_queue.pop() {
match map_queue.last_mut() { match map_queue.last_mut() {
Some((_, map)) => { Some((_, map)) => {
map.insert(key.node, Value::Map(queued_map)); map.insert(key, Value::Map(queued_map));
} }
None => { None => {
map1.insert(key.node, Value::Map(queued_map)); map1.insert(key, Value::Map(queued_map));
break; break;
} }
} }
@ -163,7 +155,7 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
Ok(Value::Map(map1)) Ok(Value::Map(map1))
} }
pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let mut map = match args.get_err(0, "map")? { let mut map = match args.get_err(0, "map")? {
Value::Map(m) => m, Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(), Value::List(v, ..) if v.is_empty() => SassMap::new(),
@ -183,7 +175,7 @@ pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser) -> SassResult<
Ok(Value::Map(map)) Ok(Value::Map(map))
} }
pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let key_position = args.len().saturating_sub(2); let key_position = args.len().saturating_sub(2);
let value_position = args.len().saturating_sub(1); let value_position = args.len().saturating_sub(1);
@ -200,7 +192,10 @@ pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
} }
}; };
let key = args.get_err(key_position, "key")?; let key = Spanned {
node: args.get_err(key_position, "key")?,
span: args.span(),
};
let value = args.get_err(value_position, "value")?; let value = args.get_err(value_position, "value")?;
let keys = args.get_variadic()?; let keys = args.get_variadic()?;
@ -232,10 +227,10 @@ pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
while let Some((key, queued_map)) = map_queue.pop() { while let Some((key, queued_map)) = map_queue.pop() {
match map_queue.last_mut() { match map_queue.last_mut() {
Some((_, next_map)) => { Some((_, next_map)) => {
next_map.insert(key.node, Value::Map(queued_map)); next_map.insert(key, Value::Map(queued_map));
} }
None => { None => {
map.insert(key.node, Value::Map(queued_map)); map.insert(key, Value::Map(queued_map));
break; break;
} }
} }

View File

@ -1,25 +1,14 @@
use super::{Builtin, GlobalFunctionMap}; use crate::{builtin::builtin_imports::*, evaluate::div};
#[cfg(feature = "random")] pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
use num_traits::{One, Signed, ToPrimitive, Zero};
#[cfg(feature = "random")]
use rand::Rng;
use crate::{
args::CallArgs,
common::Op,
error::SassResult,
parse::{HigherIntermediateValue, Parser, ValueVisitor},
unit::Unit,
value::{Number, Value},
};
pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let num = match args.get_err(0, "number")? { let num = match args.get_err(0, "number")? {
Value::Dimension(Some(n), Unit::None, _) => Some(n * Number::from(100)), Value::Dimension(SassNumber {
Value::Dimension(None, Unit::None, _) => None, num: n,
v @ Value::Dimension(..) => { unit: Unit::None,
as_slash: _,
}) => n * Number::from(100),
v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to have no units.", "$number: Expected {} to have no units.",
@ -37,14 +26,29 @@ pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<
.into()) .into())
} }
}; };
Ok(Value::Dimension(num, Unit::Percent, true)) Ok(Value::Dimension(SassNumber {
num,
unit: Unit::Percent,
as_slash: None,
}))
} }
pub(crate) fn round(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn round(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.round()), u, true)), // todo: better error message, consider finities
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
Err(("Infinity or NaN toInt", args.span()).into())
}
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => Ok(Value::Dimension(SassNumber {
num: (n.round()),
unit: u,
as_slash: None,
})),
v => Err(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -53,11 +57,22 @@ pub(crate) fn round(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
} }
} }
pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn ceil(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.ceil()), u, true)), // todo: better error message, consider finities
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
Err(("Infinity or NaN toInt", args.span()).into())
}
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => Ok(Value::Dimension(SassNumber {
num: (n.ceil()),
unit: u,
as_slash: None,
})),
v => Err(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -66,11 +81,22 @@ pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
} }
pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn floor(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.floor()), u, true)), // todo: better error message, consider finities
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
Err(("Infinity or NaN toInt", args.span()).into())
}
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => Ok(Value::Dimension(SassNumber {
num: (n.floor()),
unit: u,
as_slash: None,
})),
v => Err(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -79,11 +105,18 @@ pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
} }
} }
pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn abs(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.abs()), u, true)), Value::Dimension(SassNumber {
Value::Dimension(None, u, ..) => Ok(Value::Dimension(None, u, true)), num: n,
unit: u,
as_slash: _,
}) => Ok(Value::Dimension(SassNumber {
num: (n.abs()),
unit: u,
as_slash: None,
})),
v => Err(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -92,10 +125,14 @@ pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
} }
} }
pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn comparable(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let unit1 = match args.get_err(0, "number1")? { let unit1 = match args.get_err(0, "number1")? {
Value::Dimension(_, u, _) => u, Value::Dimension(SassNumber {
num: _,
unit: u,
as_slash: _,
}) => u,
v => { v => {
return Err(( return Err((
format!("$number1: {} is not a number.", v.inspect(args.span())?), format!("$number1: {} is not a number.", v.inspect(args.span())?),
@ -105,7 +142,11 @@ pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<
} }
}; };
let unit2 = match args.get_err(1, "number2")? { let unit2 = match args.get_err(1, "number2")? {
Value::Dimension(_, u, _) => u, Value::Dimension(SassNumber {
num: _,
unit: u,
as_slash: _,
}) => u,
v => { v => {
return Err(( return Err((
format!("$number2: {} is not a number.", v.inspect(args.span())?), format!("$number2: {} is not a number.", v.inspect(args.span())?),
@ -120,40 +161,28 @@ pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<
// TODO: write tests for this // TODO: write tests for this
#[cfg(feature = "random")] #[cfg(feature = "random")]
pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn random(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let limit = match args.default_arg(0, "limit", Value::Null)? { let limit = args.default_arg(0, "limit", Value::Null);
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, u, ..) => { if matches!(limit, Value::Null) {
return Err((format!("$limit: NaN{} is not an int.", u), args.span()).into())
}
Value::Null => {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
return Ok(Value::Dimension( return Ok(Value::Dimension(SassNumber {
Some(Number::from(rng.gen_range(0.0..1.0))), num: (Number::from(rng.gen_range(0.0..1.0))),
Unit::None, unit: Unit::None,
true, as_slash: None,
)); }));
} }
v => {
return Err(( let limit = limit.assert_number_with_name("limit", args.span())?.num();
format!("$limit: {} is not a number.", v.inspect(args.span())?), let limit_int = limit.assert_int_with_name("limit", args.span())?;
args.span(),
)
.into())
}
};
if limit.is_one() { if limit.is_one() {
return Ok(Value::Dimension(Some(Number::one()), Unit::None, true)); return Ok(Value::Dimension(SassNumber {
} num: (Number::one()),
unit: Unit::None,
if limit.is_decimal() { as_slash: None,
return Err(( }));
format!("$limit: {} is not an int.", limit.inspect()),
args.span(),
)
.into());
} }
if limit.is_zero() || limit.is_negative() { if limit.is_zero() || limit.is_negative() {
@ -164,134 +193,112 @@ pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
.into()); .into());
} }
let limit = match limit.to_integer().to_u32() {
Some(n) => n,
None => {
return Err((
format!(
"max must be in range 0 < max \u{2264} 2^32, was {}",
limit.inspect()
),
args.span(),
)
.into())
}
};
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
Ok(Value::Dimension( Ok(Value::Dimension(SassNumber {
Some(Number::from(rng.gen_range(0..limit) + 1)), num: (Number::from(rng.gen_range(0..limit_int) + 1)),
Unit::None, unit: Unit::None,
true, as_slash: None,
)) }))
} }
pub(crate) fn min(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn min(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.min_args(1)?; args.min_args(1)?;
let span = args.span(); let span = args.span();
let mut nums = args let mut nums = args
.get_variadic()? .get_variadic()?
.into_iter() .into_iter()
.map(|val| match val.node { .map(|val| match val.node {
Value::Dimension(number, unit, _) => Ok((number, unit)), Value::Dimension(SassNumber {
num: number,
unit,
as_slash: _,
}) => Ok((number, unit)),
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
}) })
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()? .collect::<SassResult<Vec<(Number, Unit)>>>()?
.into_iter(); .into_iter();
let mut min = match nums.next() { let mut min = match nums.next() {
Some((Some(n), u)) => (n, u), Some((n, u)) => (n, u),
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
None => unreachable!(), None => unreachable!(),
}; };
for (num, unit) in nums { for (num, unit) in nums {
let num = match num { let lhs = Value::Dimension(SassNumber {
Some(n) => n, num,
None => continue, unit: unit.clone(),
}; as_slash: None,
});
let rhs = Value::Dimension(SassNumber {
num: (min.0),
unit: min.1.clone(),
as_slash: None,
});
if ValueVisitor::new(parser, span) if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::LessThan)?.is_true() {
.less_than(
HigherIntermediateValue::Literal(Value::Dimension(
Some(num.clone()),
unit.clone(),
true,
)),
HigherIntermediateValue::Literal(Value::Dimension(
Some(min.0.clone()),
min.1.clone(),
true,
)),
)?
.is_true()
{
min = (num, unit); min = (num, unit);
} }
} }
Ok(Value::Dimension(Some(min.0), min.1, true)) Ok(Value::Dimension(SassNumber {
num: (min.0),
unit: min.1,
as_slash: None,
}))
} }
pub(crate) fn max(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn max(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.min_args(1)?; args.min_args(1)?;
let span = args.span(); let span = args.span();
let mut nums = args let mut nums = args
.get_variadic()? .get_variadic()?
.into_iter() .into_iter()
.map(|val| match val.node { .map(|val| match val.node {
Value::Dimension(number, unit, _) => Ok((number, unit)), Value::Dimension(SassNumber {
num: number,
unit,
as_slash: _,
}) => Ok((number, unit)),
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
}) })
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()? .collect::<SassResult<Vec<(Number, Unit)>>>()?
.into_iter(); .into_iter();
let mut max = match nums.next() { let mut max = match nums.next() {
Some((Some(n), u)) => (n, u), Some((n, u)) => (n, u),
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
None => unreachable!(), None => unreachable!(),
}; };
for (num, unit) in nums { for (num, unit) in nums {
let num = match num { let lhs = Value::Dimension(SassNumber {
Some(n) => n, num,
None => continue, unit: unit.clone(),
}; as_slash: None,
});
let rhs = Value::Dimension(SassNumber {
num: (max.0),
unit: max.1.clone(),
as_slash: None,
});
if ValueVisitor::new(parser, span) if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::GreaterThan)?.is_true()
.greater_than(
HigherIntermediateValue::Literal(Value::Dimension(
Some(num.clone()),
unit.clone(),
true,
)),
HigherIntermediateValue::Literal(Value::Dimension(
Some(max.0.clone()),
max.1.clone(),
true,
)),
)?
.is_true()
{ {
max = (num, unit); max = (num, unit);
} }
} }
Ok(Value::Dimension(Some(max.0), max.1, true)) Ok(Value::Dimension(SassNumber {
num: (max.0),
unit: max.1,
as_slash: None,
}))
} }
pub(crate) fn divide(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn divide(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let number1 = args.get_err(0, "number1")?; let number1 = args.get_err(0, "number1")?;
let number2 = args.get_err(1, "number2")?; let number2 = args.get_err(1, "number2")?;
ValueVisitor::new(parser, args.span()).eval( div(number1, number2, visitor.options, args.span())
HigherIntermediateValue::BinaryOp(
Box::new(HigherIntermediateValue::Literal(number1)),
Op::Div,
Box::new(HigherIntermediateValue::Literal(number2)),
),
true,
)
} }
pub(crate) fn declare(f: &mut GlobalFunctionMap) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {

View File

@ -1,17 +1,28 @@
use super::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS}; use crate::builtin::builtin_imports::*;
use codemap::Spanned; // todo: this should be a constant of some sort. we shouldn't be allocating this
// every time
pub(crate) fn if_arguments() -> ArgumentDeclaration {
ArgumentDeclaration {
args: vec![
Argument {
name: Identifier::from("condition"),
default: None,
},
Argument {
name: Identifier::from("if-true"),
default: None,
},
Argument {
name: Identifier::from("if-false"),
default: None,
},
],
rest: None,
}
}
use crate::{ fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args::CallArgs,
common::{Identifier, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{SassFunction, Value},
};
fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
if args.get_err(0, "condition")?.is_true() { if args.get_err(0, "condition")?.is_true() {
Ok(args.get_err(1, "if-true")?) Ok(args.get_err(1, "if-true")?)
@ -20,7 +31,7 @@ fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
} }
} }
pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn feature_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "feature")? { match args.get_err(0, "feature")? {
#[allow(clippy::match_same_arms)] #[allow(clippy::match_same_arms)]
@ -39,7 +50,7 @@ pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassRes
// The "Custom Properties Level 1" spec is supported. This means // The "Custom Properties Level 1" spec is supported. This means
// that custom properties are parsed statically, with only // that custom properties are parsed statically, with only
// interpolation treated as SassScript. // interpolation treated as SassScript.
"custom-property" => Value::False, "custom-property" => Value::True,
_ => Value::False, _ => Value::False,
}), }),
v => Err(( v => Err((
@ -50,10 +61,14 @@ pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassRes
} }
} }
pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn unit(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let unit = match args.get_err(0, "number")? { let unit = match args.get_err(0, "number")? {
Value::Dimension(_, u, _) => u.to_string(), Value::Dimension(SassNumber {
num: _,
unit: u,
as_slash: _,
}) => u.to_string(),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -65,17 +80,21 @@ pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
Ok(Value::String(unit, QuoteKind::Quoted)) Ok(Value::String(unit, QuoteKind::Quoted))
} }
pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn type_of(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let value = args.get_err(0, "value")?; let value = args.get_err(0, "value")?;
Ok(Value::String(value.kind().to_owned(), QuoteKind::None)) Ok(Value::String(value.kind().to_owned(), QuoteKind::None))
} }
pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(match args.get_err(0, "number")? { Ok(match args.get_err(0, "number")? {
Value::Dimension(_, Unit::None, _) => Value::True, Value::Dimension(SassNumber {
Value::Dimension(..) => Value::False, num: _,
unit: Unit::None,
as_slash: _,
}) => Value::True,
Value::Dimension(SassNumber { .. }) => Value::False,
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -86,7 +105,7 @@ pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser) -> SassResult<Va
}) })
} }
pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::String( Ok(Value::String(
args.get_err(0, "value")?.inspect(args.span())?.into_owned(), args.get_err(0, "value")?.inspect(args.span())?.into_owned(),
@ -94,12 +113,13 @@ pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
)) ))
} }
pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn variable_exists(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "name")? { match args.get_err(0, "name")? {
Value::String(s, _) => Ok(Value::bool( Value::String(s, _) => Ok(Value::bool(visitor.env.var_exists(s.into(), None)?)),
parser.scopes.var_exists(s.into(), parser.global_scope),
)),
v => Err(( v => Err((
format!("$name: {} is not a string.", v.inspect(args.span())?), format!("$name: {} is not a string.", v.inspect(args.span())?),
args.span(), args.span(),
@ -108,7 +128,10 @@ pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
} }
} }
pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn global_variable_exists(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let name: Identifier = match args.get_err(0, "name")? { let name: Identifier = match args.get_err(0, "name")? {
@ -122,7 +145,7 @@ pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) ->
} }
}; };
let module = match args.default_arg(1, "module", Value::Null)? { let module = match args.default_arg(1, "module", Value::Null) {
Value::String(s, _) => Some(s), Value::String(s, _) => Some(s),
Value::Null => None, Value::Null => None,
v => { v => {
@ -135,16 +158,17 @@ pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) ->
}; };
Ok(Value::bool(if let Some(module_name) = module { Ok(Value::bool(if let Some(module_name) = module {
parser (*(*visitor.env.modules)
.modules .borrow()
.get(module_name.into(), args.span())? .get(module_name.into(), args.span())?)
.borrow()
.var_exists(name) .var_exists(name)
} else { } else {
parser.global_scope.var_exists(name) (*visitor.env.global_vars()).borrow().contains_key(&name)
})) }))
} }
pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn mixin_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let name: Identifier = match args.get_err(0, "name")? { let name: Identifier = match args.get_err(0, "name")? {
Value::String(s, _) => s.into(), Value::String(s, _) => s.into(),
@ -157,7 +181,7 @@ pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResul
} }
}; };
let module = match args.default_arg(1, "module", Value::Null)? { let module = match args.default_arg(1, "module", Value::Null) {
Value::String(s, _) => Some(s), Value::String(s, _) => Some(s),
Value::Null => None, Value::Null => None,
v => { v => {
@ -170,16 +194,20 @@ pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResul
}; };
Ok(Value::bool(if let Some(module_name) = module { Ok(Value::bool(if let Some(module_name) = module {
parser (*(*visitor.env.modules)
.modules .borrow()
.get(module_name.into(), args.span())? .get(module_name.into(), args.span())?)
.borrow()
.mixin_exists(name) .mixin_exists(name)
} else { } else {
parser.scopes.mixin_exists(name, parser.global_scope) visitor.env.mixin_exists(name)
})) }))
} }
pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn function_exists(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let name: Identifier = match args.get_err(0, "name")? { let name: Identifier = match args.get_err(0, "name")? {
@ -193,7 +221,7 @@ pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
} }
}; };
let module = match args.default_arg(1, "module", Value::Null)? { let module = match args.default_arg(1, "module", Value::Null) {
Value::String(s, _) => Some(s), Value::String(s, _) => Some(s),
Value::Null => None, Value::Null => None,
v => { v => {
@ -206,16 +234,17 @@ pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
}; };
Ok(Value::bool(if let Some(module_name) = module { Ok(Value::bool(if let Some(module_name) = module {
parser (*(*visitor.env.modules)
.modules .borrow()
.get(module_name.into(), args.span())? .get(module_name.into(), args.span())?)
.borrow()
.fn_exists(name) .fn_exists(name)
} else { } else {
parser.scopes.fn_exists(name, parser.global_scope) visitor.env.fn_exists(name)
})) }))
} }
pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let name: Identifier = match args.get_err(0, "name")? { let name: Identifier = match args.get_err(0, "name")? {
Value::String(s, _) => s.into(), Value::String(s, _) => s.into(),
@ -227,8 +256,8 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
.into()) .into())
} }
}; };
let css = args.default_arg(1, "css", Value::False)?.is_true(); let css = args.default_arg(1, "css", Value::False).is_true();
let module = match args.default_arg(2, "module", Value::Null)? { let module = match args.default_arg(2, "module", Value::Null) {
Value::String(s, ..) => Some(s), Value::String(s, ..) => Some(s),
Value::Null => None, Value::Null => None,
v => { v => {
@ -240,7 +269,7 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
} }
}; };
let func = match if let Some(module_name) = module { let func = if let Some(module_name) = module {
if css { if css {
return Err(( return Err((
"$css and $module may not both be passed at once.", "$css and $module may not both be passed at once.",
@ -249,68 +278,90 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
.into()); .into());
} }
parser visitor.env.get_fn(
.modules name,
.get(module_name.into(), args.span())? Some(Spanned {
.get_fn(Spanned { node: module_name.into(),
node: name,
span: args.span(), span: args.span(),
})? }),
)?
} else { } else {
parser.scopes.get_fn(name, parser.global_scope) match visitor.env.get_fn(name, None)? {
} { Some(f) => Some(f),
Some(f) => f, None => GLOBAL_FUNCTIONS
None => match GLOBAL_FUNCTIONS.get(name.as_str()) { .get(name.as_str())
Some(f) => SassFunction::Builtin(f.clone(), name), .map(|f| SassFunction::Builtin(f.clone(), name)),
None => return Err((format!("Function not found: {}", name), args.span()).into()), }
},
}; };
Ok(Value::FunctionRef(func)) match func {
Some(func) => Ok(Value::FunctionRef(func)),
None => Err((format!("Function not found: {}", name), args.span()).into()),
}
} }
pub(crate) fn call(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span();
let func = match args.get_err(0, "function")? { let func = match args.get_err(0, "function")? {
Value::FunctionRef(f) => f, Value::FunctionRef(f) => f,
v => { v => {
return Err(( return Err((
format!( format!(
"$function: {} is not a function reference.", "$function: {} is not a function reference.",
v.inspect(args.span())? v.inspect(span)?
), ),
args.span(), span,
) )
.into()) .into())
} }
}; };
func.call(args.decrement(), None, parser)
args.remove_positional(0).unwrap();
visitor.run_function_callable_with_maybe_evaled(func, MaybeEvaledArguments::Evaled(args), span)
} }
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub(crate) fn content_exists(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn content_exists(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(0)?; args.max_args(0)?;
if !parser.flags.in_mixin() { if !visitor.flags.in_mixin() {
return Err(( return Err((
"content-exists() may only be called within a mixin.", "content-exists() may only be called within a mixin.",
parser.span_before, args.span(),
) )
.into()); .into());
} }
Ok(Value::bool( Ok(Value::bool(visitor.env.content.is_some()))
parser.content.last().map_or(false, |c| c.content.is_some()),
))
} }
#[allow(unused_variables, clippy::needless_pass_by_value)] pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
pub(crate) fn keywords(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Err(( let span = args.span();
"Builtin function `keywords` is not yet implemented",
args.span(), let args = match args.get_err(0, "args")? {
Value::ArgList(args) => args,
v => {
return Err((
format!("$args: {} is not an argument list.", v.inspect(span)?),
span,
) )
.into()) .into())
} }
};
Ok(Value::Map(SassMap::new_with(
args.into_keywords()
.into_iter()
.map(|(name, val)| {
(
Value::String(name.to_string(), QuoteKind::None).span(span),
val,
)
})
.collect(),
)))
}
pub(crate) fn declare(f: &mut GlobalFunctionMap) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {
f.insert("if", Builtin::new(if_)); f.insert("if", Builtin::new(if_));

View File

@ -2,13 +2,13 @@
#![allow(unused_variables)] #![allow(unused_variables)]
use std::{ use std::{
collections::HashMap, collections::{BTreeSet, HashMap},
sync::atomic::{AtomicUsize, Ordering}, sync::atomic::{AtomicUsize, Ordering},
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::{args::CallArgs, error::SassResult, parse::Parser, value::Value}; use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value};
#[macro_use] #[macro_use]
mod macros; mod macros;
@ -21,16 +21,19 @@ pub mod meta;
pub mod selector; pub mod selector;
pub mod string; pub mod string;
// todo: maybe Identifier instead of str?
pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>; pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>;
static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0); static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0);
// TODO: impl Fn
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Builtin(pub fn(CallArgs, &mut Parser) -> SassResult<Value>, usize); pub(crate) struct Builtin(
pub fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
usize,
);
impl Builtin { impl Builtin {
pub fn new(body: fn(CallArgs, &mut Parser) -> SassResult<Value>) -> Builtin { pub fn new(body: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>) -> Builtin {
let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed); let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed);
Self(body, count) Self(body, count)
} }
@ -55,3 +58,24 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy<GlobalFunctionMap> = Lazy::new(|| {
string::declare(&mut m); string::declare(&mut m);
m m
}); });
pub(crate) static DISALLOWED_PLAIN_CSS_FUNCTION_NAMES: Lazy<BTreeSet<&str>> = Lazy::new(|| {
GLOBAL_FUNCTIONS
.keys()
.copied()
.filter(|&name| {
!matches!(
name,
"rgb"
| "rgba"
| "hsl"
| "hsla"
| "grayscale"
| "invert"
| "alpha"
| "opacity"
| "saturate"
)
})
.collect()
});

View File

@ -1,32 +1,36 @@
use super::{Builtin, GlobalFunctionMap}; use crate::builtin::builtin_imports::*;
use crate::{ use crate::selector::{
args::CallArgs, ComplexSelector, ComplexSelectorComponent, ExtensionStore, Selector, SelectorList,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
selector::{ComplexSelector, ComplexSelectorComponent, Extender, Selector, SelectorList},
value::Value,
}; };
use crate::serializer::serialize_selector_list;
pub(crate) fn is_superselector(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn is_superselector(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let parent_selector = args let parent_selector =
.get_err(0, "super")? args.get_err(0, "super")?
.to_selector(parser, "super", false)?; .to_selector(visitor, "super", false, args.span())?;
let child_selector = args.get_err(1, "sub")?.to_selector(parser, "sub", false)?; let child_selector = args
.get_err(1, "sub")?
.to_selector(visitor, "sub", false, args.span())?;
Ok(Value::bool( Ok(Value::bool(
parent_selector.is_super_selector(&child_selector), parent_selector.is_super_selector(&child_selector),
)) ))
} }
pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn simple_selectors(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
// todo: Value::to_compound_selector // todo: Value::to_compound_selector
let selector = args let selector =
.get_err(0, "selector")? args.get_err(0, "selector")?
.to_selector(parser, "selector", false)?; .to_selector(visitor, "selector", false, args.span())?;
if selector.0.components.len() != 1 { if selector.0.components.len() != 1 {
return Err(("$selector: expected selector.", args.span()).into()); return Err(("$selector: expected selector.", args.span()).into());
@ -51,16 +55,16 @@ pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser) -> SassR
)) ))
} }
pub(crate) fn selector_parse(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn selector_parse(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(args Ok(args
.get_err(0, "selector")? .get_err(0, "selector")?
.to_selector(parser, "selector", false) .to_selector(visitor, "selector", false, args.span())
.map_err(|_| ("$selector: expected selector.", args.span()))? .map_err(|_| ("$selector: expected selector.", args.span()))?
.into_value()) .into_value())
} }
pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn selector_nest(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span(); let span = args.span();
let selectors = args.get_variadic()?; let selectors = args.get_variadic()?;
if selectors.is_empty() { if selectors.is_empty() {
@ -69,7 +73,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
Ok(selectors Ok(selectors
.into_iter() .into_iter()
.map(|sel| sel.node.to_selector(parser, "selectors", true)) .map(|sel| sel.node.to_selector(visitor, "selectors", true, span))
.collect::<SassResult<Vec<Selector>>>()? .collect::<SassResult<Vec<Selector>>>()?
.into_iter() .into_iter()
.try_fold( .try_fold(
@ -81,7 +85,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
.into_value()) .into_value())
} }
pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn selector_append(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span(); let span = args.span();
let selectors = args.get_variadic()?; let selectors = args.get_variadic()?;
if selectors.is_empty() { if selectors.is_empty() {
@ -90,7 +94,7 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
let mut parsed_selectors = selectors let mut parsed_selectors = selectors
.into_iter() .into_iter()
.map(|s| s.node.to_selector(parser, "selectors", false)) .map(|s| s.node.to_selector(visitor, "selectors", false, span))
.collect::<SassResult<Vec<Selector>>>()?; .collect::<SassResult<Vec<Selector>>>()?;
let first = parsed_selectors.remove(0); let first = parsed_selectors.remove(0);
@ -109,7 +113,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
Some(v) => ComplexSelectorComponent::Compound(v), Some(v) => ComplexSelectorComponent::Compound(v),
None => { None => {
return Err(( return Err((
format!("Can't append {} to {}.", complex, parent), format!(
"Can't append {} to {}.",
complex,
serialize_selector_list(
&parent.0,
visitor.options,
span
)
),
span, span,
) )
.into()) .into())
@ -118,7 +130,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
components.extend(complex.components.into_iter().skip(1)); components.extend(complex.components.into_iter().skip(1));
Ok(ComplexSelector::new(components, false)) Ok(ComplexSelector::new(components, false))
} else { } else {
Err((format!("Can't append {} to {}.", complex, parent), span).into()) Err((
format!(
"Can't append {} to {}.",
complex,
serialize_selector_list(&parent.0, visitor.options, span)
),
span,
)
.into())
} }
}) })
.collect::<SassResult<Vec<ComplexSelector>>>()?, .collect::<SassResult<Vec<ComplexSelector>>>()?,
@ -129,40 +149,46 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
.into_value()) .into_value())
} }
pub(crate) fn selector_extend(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn selector_extend(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let selector = args let selector =
.get_err(0, "selector")? args.get_err(0, "selector")?
.to_selector(parser, "selector", false)?; .to_selector(visitor, "selector", false, args.span())?;
let target = args let target =
.get_err(1, "extendee")? args.get_err(1, "extendee")?
.to_selector(parser, "extendee", false)?; .to_selector(visitor, "extendee", false, args.span())?;
let source = args let source =
.get_err(2, "extender")? args.get_err(2, "extender")?
.to_selector(parser, "extender", false)?; .to_selector(visitor, "extender", false, args.span())?;
Ok(Extender::extend(selector.0, source.0, target.0, args.span())?.to_sass_list()) Ok(ExtensionStore::extend(selector.0, source.0, target.0, args.span())?.to_sass_list())
} }
pub(crate) fn selector_replace(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn selector_replace(
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let selector = args let selector =
.get_err(0, "selector")? args.get_err(0, "selector")?
.to_selector(parser, "selector", true)?; .to_selector(visitor, "selector", true, args.span())?;
let target = args let target =
.get_err(1, "original")? args.get_err(1, "original")?
.to_selector(parser, "original", true)?; .to_selector(visitor, "original", true, args.span())?;
let source = args let source =
.get_err(2, "replacement")? args.get_err(2, "replacement")?
.to_selector(parser, "replacement", true)?; .to_selector(visitor, "replacement", true, args.span())?;
Ok(Extender::replace(selector.0, source.0, target.0, args.span())?.to_sass_list()) Ok(ExtensionStore::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
} }
pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn selector_unify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let selector1 = args let selector1 =
.get_err(0, "selector1")? args.get_err(0, "selector1")?
.to_selector(parser, "selector1", true)?; .to_selector(visitor, "selector1", true, args.span())?;
if selector1.contains_parent_selector() { if selector1.contains_parent_selector() {
return Err(( return Err((
@ -172,9 +198,9 @@ pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassRes
.into()); .into());
} }
let selector2 = args let selector2 =
.get_err(1, "selector2")? args.get_err(1, "selector2")?
.to_selector(parser, "selector2", true)?; .to_selector(visitor, "selector2", true, args.span())?;
if selector2.contains_parent_selector() { if selector2.contains_parent_selector() {
return Err(( return Err((

View File

@ -1,21 +1,6 @@
use super::{Builtin, GlobalFunctionMap}; use crate::builtin::builtin_imports::*;
use num_bigint::BigInt; pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
use num_traits::{Signed, ToPrimitive, Zero};
#[cfg(feature = "random")]
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use crate::{
args::CallArgs,
common::QuoteKind,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(mut i, q) => { Value::String(mut i, q) => {
@ -30,7 +15,7 @@ pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResu
} }
} }
pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn to_lower_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(mut i, q) => { Value::String(mut i, q) => {
@ -45,14 +30,14 @@ pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResu
} }
} }
pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn str_length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(i, _) => Ok(Value::Dimension( Value::String(i, _) => Ok(Value::Dimension(SassNumber {
Some(Number::from(i.chars().count())), num: (Number::from(i.chars().count())),
Unit::None, unit: Unit::None,
true, as_slash: None,
)), })),
v => Err(( v => Err((
format!("$string: {} is not a string.", v.inspect(args.span())?), format!("$string: {} is not a string.", v.inspect(args.span())?),
args.span(), args.span(),
@ -61,7 +46,7 @@ pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<
} }
} }
pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn quote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)), Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)),
@ -73,7 +58,7 @@ pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
} }
} }
pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn unquote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
i @ Value::String(..) => Ok(i.unquote()), i @ Value::String(..) => Ok(i.unquote()),
@ -85,8 +70,11 @@ pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
} }
} }
pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let span = args.span();
let (string, quotes) = match args.get_err(0, "string")? { let (string, quotes) = match args.get_err(0, "string")? {
Value::String(s, q) => (s, q), Value::String(s, q) => (s, q),
v => { v => {
@ -97,79 +85,46 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
.into()) .into())
} }
}; };
let str_len = string.chars().count(); let str_len = string.chars().count();
let start = match args.get_err(1, "start-at")? {
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { let start = args
return Err((format!("{} is not an int.", n.inspect()), args.span()).into()) .get_err(1, "start-at")?
} .assert_number_with_name("start-at", span)?;
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { start.assert_no_units("start-at", span)?;
n.to_integer().to_usize().unwrap_or(str_len + 1)
} let start = start.num().assert_int(span)?;
Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize,
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize, let start = if start == 0 {
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) 1
.to_usize() } else if start > 0 {
.unwrap(), (start as usize).min(str_len + 1)
Value::Dimension(None, Unit::None, ..) => { } else {
return Err(("NaN is not an int.", args.span()).into()) (start + str_len as i32 + 1).max(1) as usize
}
v @ Value::Dimension(..) => {
return Err((
format!(
"$start: Expected {} to have no units.",
v.inspect(args.span())?
),
args.span(),
)
.into())
}
v => {
return Err((
format!("$start-at: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let mut end = match args.default_arg(2, "end-at", Value::Null)? {
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
}
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
n.to_integer().to_usize().unwrap_or(str_len + 1)
}
Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize,
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 0_usize,
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
.to_usize()
.unwrap_or(str_len + 1),
Value::Dimension(None, Unit::None, ..) => {
return Err(("NaN is not an int.", args.span()).into())
}
v @ Value::Dimension(..) => {
return Err((
format!(
"$end: Expected {} to have no units.",
v.inspect(args.span())?
),
args.span(),
)
.into())
}
Value::Null => str_len,
v => {
return Err((
format!("$end-at: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
}; };
if end > str_len { let end = args
end = str_len; .default_arg(
2,
"end-at",
Value::Dimension(SassNumber {
num: Number(-1.0),
unit: Unit::None,
as_slash: None,
}),
)
.assert_number_with_name("end-at", span)?;
end.assert_no_units("end-at", span)?;
let mut end = end.num().assert_int(span)?;
if end < 0 {
end += str_len as i32 + 1;
} }
let end = (end.max(0) as usize).min(str_len + 1);
if start > end || start > str_len { if start > end || start > str_len {
Ok(Value::String(String::new(), quotes)) Ok(Value::String(String::new(), quotes))
} else { } else {
@ -184,7 +139,7 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
} }
} }
pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let s1 = match args.get_err(0, "string")? { let s1 = match args.get_err(0, "string")? {
Value::String(i, _) => i, Value::String(i, _) => i,
@ -209,19 +164,25 @@ pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
}; };
Ok(match s1.find(&substr) { Ok(match s1.find(&substr) {
Some(v) => Value::Dimension(Some(Number::from(v + 1)), Unit::None, true), Some(v) => Value::Dimension(SassNumber {
num: (Number::from(v + 1)),
unit: Unit::None,
as_slash: None,
}),
None => Value::Null, None => Value::Null,
}) })
} }
pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let span = args.span();
let (s1, quotes) = match args.get_err(0, "string")? { let (s1, quotes) = match args.get_err(0, "string")? {
Value::String(i, q) => (i, q), Value::String(i, q) => (i, q),
v => { v => {
return Err(( return Err((
format!("$string: {} is not a string.", v.inspect(args.span())?), format!("$string: {} is not a string.", v.inspect(span)?),
args.span(), span,
) )
.into()) .into())
} }
@ -231,43 +192,18 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
Value::String(i, _) => i, Value::String(i, _) => i,
v => { v => {
return Err(( return Err((
format!("$insert: {} is not a string.", v.inspect(args.span())?), format!("$insert: {} is not a string.", v.inspect(span)?),
args.span(), span,
) )
.into()) .into())
} }
}; };
let index = match args.get_err(2, "index")? { let index = args
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { .get_err(2, "index")?
return Err(( .assert_number_with_name("index", span)?;
format!("$index: {} is not an int.", n.inspect()), index.assert_no_units("index", span)?;
args.span(), let index_int = index.num().assert_int_with_name("index", span)?;
)
.into())
}
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(None, Unit::None, ..) => {
return Err(("$index: NaN is not an int.", args.span()).into())
}
v @ Value::Dimension(..) => {
return Err((
format!(
"$index: Expected {} to have no units.",
v.inspect(args.span())?
),
args.span(),
)
.into())
}
v => {
return Err((
format!("$index: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
if s1.is_empty() { if s1.is_empty() {
return Ok(Value::String(substr, quotes)); return Ok(Value::String(substr, quotes));
@ -291,26 +227,13 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
.collect::<String>() .collect::<String>()
}; };
let string = if index.is_positive() { let string = if index_int > 0 {
insert( insert((index_int as usize - 1).min(len), s1, &substr)
index } else if index_int == 0 {
.to_integer()
.to_usize()
.unwrap_or(len + 1)
.min(len + 1)
- 1,
s1,
&substr,
)
} else if index.is_zero() {
insert(0, s1, &substr) insert(0, s1, &substr)
} else { } else {
let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1); let idx = (len as i32 + index_int + 1).max(0) as usize;
if idx > len { insert(idx, s1, &substr)
insert(0, s1, &substr)
} else {
insert(len - idx + 1, s1, &substr)
}
}; };
Ok(Value::String(string, quotes)) Ok(Value::String(string, quotes))
@ -318,15 +241,15 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
#[cfg(feature = "random")] #[cfg(feature = "random")]
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub(crate) fn unique_id(args: CallArgs, _: &mut Parser) -> SassResult<Value> { pub(crate) fn unique_id(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(0)?; args.max_args(0)?;
let mut rng = thread_rng(); let mut rng = thread_rng();
let string = std::iter::repeat(()) let string: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric)) .map(|()| rng.sample(Alphanumeric))
.map(char::from) .map(char::from)
.take(7) .take(12)
.collect(); .collect();
Ok(Value::String(string, QuoteKind::None)) Ok(Value::String(format!("id-{}", string), QuoteKind::None))
} }
pub(crate) fn declare(f: &mut GlobalFunctionMap) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {

View File

@ -2,5 +2,32 @@ mod functions;
pub(crate) mod modules; pub(crate) mod modules;
pub(crate) use functions::{ pub(crate) use functions::{
color, list, map, math, meta, selector, string, Builtin, GLOBAL_FUNCTIONS, color, list, map, math, meta, selector, string, Builtin, DISALLOWED_PLAIN_CSS_FUNCTION_NAMES,
GLOBAL_FUNCTIONS,
}; };
/// Imports common to all builtin fns
mod builtin_imports {
pub(crate) use super::functions::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS};
pub(crate) use codemap::{Span, Spanned};
#[cfg(feature = "random")]
pub(crate) use rand::{distributions::Alphanumeric, thread_rng, Rng};
pub(crate) use crate::{
ast::{Argument, ArgumentDeclaration, ArgumentResult, MaybeEvaledArguments},
color::Color,
common::{BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind},
error::SassResult,
evaluate::Visitor,
unit::Unit,
value::{CalculationArg, Number, SassFunction, SassMap, SassNumber, Value},
Options,
};
pub(crate) use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
};
}

View File

@ -1,8 +1,32 @@
use crate::builtin::builtin_imports::*;
use crate::builtin::{ use crate::builtin::{
list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip}, list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip},
modules::Module, modules::Module,
}; };
// todo: write tests for this
fn slash(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult<Value> {
args.min_args(1)?;
let span = args.span();
let list = if args.len() == 1 {
args.get_err(0, "elements")?.as_list()
} else {
args.get_variadic()?
.into_iter()
.map(|arg| arg.node)
.collect()
};
if list.len() < 2 {
return Err(("At least two elements are required.", span).into());
}
Ok(Value::List(list, ListSeparator::Slash, Brackets::None))
}
pub(crate) fn declare(f: &mut Module) { pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("append", append); f.insert_builtin("append", append);
f.insert_builtin("index", index); f.insert_builtin("index", index);
@ -13,4 +37,5 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("nth", nth); f.insert_builtin("nth", nth);
f.insert_builtin("set-nth", set_nth); f.insert_builtin("set-nth", set_nth);
f.insert_builtin("zip", zip); f.insert_builtin("zip", zip);
f.insert_builtin("slash", slash);
} }

View File

@ -1,30 +1,36 @@
use std::cmp::Ordering; use crate::builtin::builtin_imports::*;
use num_traits::{One, Signed, Zero}; use crate::builtin::{
use crate::{
args::CallArgs,
builtin::{
math::{abs, ceil, comparable, divide, floor, max, min, percentage, round}, math::{abs, ceil, comparable, divide, floor, max, min, percentage, round},
meta::{unit, unitless}, meta::{unit, unitless},
modules::Module, modules::Module,
},
common::Op,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
}; };
#[cfg(feature = "random")] #[cfg(feature = "random")]
use crate::builtin::math::random; use crate::builtin::math::random;
use crate::value::{conversion_factor, SassNumber};
fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn coerce_to_rad(num: f64, unit: Unit) -> f64 {
debug_assert!(matches!(
unit,
Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn
));
if unit == Unit::None {
return num;
}
let factor = conversion_factor(&unit, &Unit::Rad).unwrap();
num * factor
}
fn clamp(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let span = args.span(); let span = args.span();
let min = match args.get_err(0, "min")? { let min = match args.get_err(0, "min")? {
v @ Value::Dimension(..) => v, v @ Value::Dimension(SassNumber { .. }) => v,
v => { v => {
return Err(( return Err((
format!("$min: {} is not a number.", v.inspect(args.span())?), format!("$min: {} is not a number.", v.inspect(args.span())?),
@ -35,7 +41,7 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}; };
let number = match args.get_err(1, "number")? { let number = match args.get_err(1, "number")? {
v @ Value::Dimension(..) => v, v @ Value::Dimension(SassNumber { .. }) => v,
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(span)?), format!("$number: {} is not a number.", v.inspect(span)?),
@ -46,23 +52,35 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}; };
let max = match args.get_err(2, "max")? { let max = match args.get_err(2, "max")? {
v @ Value::Dimension(..) => v, v @ Value::Dimension(SassNumber { .. }) => v,
v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()), v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()),
}; };
// ensure that `min` and `max` are compatible // ensure that `min` and `max` are compatible
min.cmp(&max, span, Op::LessThan)?; min.cmp(&max, span, BinaryOp::LessThan)?;
let min_unit = match min { let min_unit = match min {
Value::Dimension(_, ref u, _) => u, Value::Dimension(SassNumber {
num: _,
unit: ref u,
as_slash: _,
}) => u,
_ => unreachable!(), _ => unreachable!(),
}; };
let number_unit = match number { let number_unit = match number {
Value::Dimension(_, ref u, _) => u, Value::Dimension(SassNumber {
num: _,
unit: ref u,
as_slash: _,
}) => u,
_ => unreachable!(), _ => unreachable!(),
}; };
let max_unit = match max { let max_unit = match max {
Value::Dimension(_, ref u, _) => u, Value::Dimension(SassNumber {
num: _,
unit: ref u,
as_slash: _,
}) => u,
_ => unreachable!(), _ => unreachable!(),
}; };
@ -86,45 +104,43 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
), span).into()); ), span).into());
} }
match min.cmp(&number, span, Op::LessThan)? { match min.cmp(&number, span, BinaryOp::LessThan)? {
Ordering::Greater => return Ok(min), Some(Ordering::Greater) => return Ok(min),
Ordering::Equal => return Ok(number), Some(Ordering::Equal) => return Ok(number),
Ordering::Less => {} Some(Ordering::Less) | None => {}
} }
match max.cmp(&number, span, Op::GreaterThan)? { match max.cmp(&number, span, BinaryOp::GreaterThan)? {
Ordering::Less => return Ok(max), Some(Ordering::Less) => return Ok(max),
Ordering::Equal => return Ok(number), Some(Ordering::Equal) => return Ok(number),
Ordering::Greater => {} Some(Ordering::Greater) | None => {}
} }
Ok(number) Ok(number)
} }
fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.min_args(1)?; args.min_args(1)?;
let span = args.span(); let span = args.span();
let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> { let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> {
match v.node { match v.node {
Value::Dimension(n, u, ..) => Ok((n, u)), Value::Dimension(SassNumber { num, unit, .. }) => Ok((num, unit)),
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
} }
}); });
let first: (Number, Unit) = match numbers.next().unwrap()? { let (n, u) = numbers.next().unwrap()?;
(Some(n), u) => (n.clone() * n, u), let first: (Number, Unit) = (n * n, u);
(None, u) => return Ok(Value::Dimension(None, u, true)),
};
let rest = numbers let rest = numbers
.enumerate() .enumerate()
.map(|(idx, val)| -> SassResult<Option<Number>> { .map(|(idx, val)| -> SassResult<Number> {
let (number, unit) = val?; let (number, unit) = val?;
if first.1 == Unit::None { if first.1 == Unit::None {
if unit == Unit::None { if unit == Unit::None {
Ok(number.map(|n| n.clone() * n)) Ok(number * number)
} else { } else {
Err(( Err((
format!( format!(
@ -149,9 +165,8 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} else if first.1.comparable(&unit) { } else if first.1.comparable(&unit) {
Ok(number let n = number.convert(&unit, &first.1);
.map(|n| n.convert(&unit, &first.1)) Ok(n * n)
.map(|n| n.clone() * n))
} else { } else {
Err(( Err((
format!("Incompatible units {} and {}.", first.1, unit), format!("Incompatible units {} and {}.", first.1, unit),
@ -160,24 +175,28 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
.into()) .into())
} }
}) })
.collect::<SassResult<Option<Vec<Number>>>>()?; .collect::<SassResult<Vec<Number>>>()?;
let rest = match rest {
Some(v) => v,
None => return Ok(Value::Dimension(None, first.1, true)),
};
let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b); let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b);
Ok(Value::Dimension(sum.sqrt(), first.1, true)) Ok(Value::Dimension(SassNumber {
num: sum.sqrt(),
unit: first.1,
as_slash: None,
}))
} }
fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let number = match args.get_err(0, "number")? { let number = match args.get_err(0, "number")? {
Value::Dimension(Some(n), Unit::None, ..) => n, // Value::Dimension { num: n, .. } if n.is_nan() => todo!(),
v @ Value::Dimension(Some(..), ..) => { Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => num,
v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to be unitless.", "$number: Expected {} to be unitless.",
@ -187,7 +206,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
v @ Value::Dimension(None, ..) => return Ok(v),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -197,10 +215,14 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
} }
}; };
let base = match args.default_arg(1, "base", Value::Null)? { let base = match args.default_arg(1, "base", Value::Null) {
Value::Null => None, Value::Null => None,
Value::Dimension(Some(n), Unit::None, ..) => Some(n), Value::Dimension(SassNumber {
v @ Value::Dimension(Some(..), ..) => { num,
unit: Unit::None,
..
}) => Some(num),
v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to be unitless.", "$number: Expected {} to be unitless.",
@ -210,7 +232,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
v @ Value::Dimension(None, ..) => return Ok(v),
v => { v => {
return Err(( return Err((
format!("$base: {} is not a number.", v.inspect(args.span())?), format!("$base: {} is not a number.", v.inspect(args.span())?),
@ -220,31 +241,36 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
} }
}; };
Ok(Value::Dimension( Ok(Value::Dimension(SassNumber {
if let Some(base) = base { num: if let Some(base) = base {
if base.is_zero() { if base.is_zero() {
Some(Number::zero()) Number::zero()
} else { } else {
number.log(base) number.log(base)
} }
} else if number.is_negative() { // todo: test with negative 0
None } else if number.is_negative() && !number.is_zero() {
Number(f64::NAN)
} else if number.is_zero() { } else if number.is_zero() {
todo!() Number(f64::NEG_INFINITY)
} else { } else {
number.ln() number.ln()
}, },
Unit::None, unit: Unit::None,
true, as_slash: None,
)) }))
} }
fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let base = match args.get_err(0, "base")? { let base = match args.get_err(0, "base")? {
Value::Dimension(Some(n), Unit::None, ..) => n, Value::Dimension(SassNumber {
v @ Value::Dimension(Some(..), ..) => { num,
unit: Unit::None,
..
}) => num,
v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$base: Expected {} to have no units.", "$base: Expected {} to have no units.",
@ -254,7 +280,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
v => { v => {
return Err(( return Err((
format!("$base: {} is not a number.", v.inspect(args.span())?), format!("$base: {} is not a number.", v.inspect(args.span())?),
@ -265,8 +290,12 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}; };
let exponent = match args.get_err(1, "exponent")? { let exponent = match args.get_err(1, "exponent")? {
Value::Dimension(Some(n), Unit::None, ..) => n, Value::Dimension(SassNumber {
v @ Value::Dimension(Some(..), ..) => { num,
unit: Unit::None,
..
}) => num,
v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$exponent: Expected {} to have no units.", "$exponent: Expected {} to have no units.",
@ -276,7 +305,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
v => { v => {
return Err(( return Err((
format!("$exponent: {} is not a number.", v.inspect(args.span())?), format!("$exponent: {} is not a number.", v.inspect(args.span())?),
@ -286,16 +314,28 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
} }
}; };
Ok(Value::Dimension(base.pow(exponent), Unit::None, true)) Ok(Value::Dimension(SassNumber {
num: base.pow(exponent),
unit: Unit::None,
as_slash: None,
}))
} }
fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?; let number = args.get_err(0, "number")?;
Ok(match number { Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(n.sqrt(), Unit::None, true), Value::Dimension(SassNumber {
v @ Value::Dimension(Some(..), ..) => { num,
unit: Unit::None,
..
}) => Value::Dimension(SassNumber {
num: num.sqrt(),
unit: Unit::None,
as_slash: None,
}),
v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to have no units.", "$number: Expected {} to have no units.",
@ -305,7 +345,6 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -317,30 +356,32 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
} }
macro_rules! trig_fn { macro_rules! trig_fn {
($name:ident, $name_deg:ident) => { ($name:ident) => {
fn $name(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?; let number = args.get_err(0, "number")?;
Ok(match number { Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
| Value::Dimension(Some(n), Unit::Rad, ..) => { Value::Dimension(SassNumber {
Value::Dimension(n.$name(), Unit::None, true) num,
} unit: unit @ (Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn),
Value::Dimension(Some(n), Unit::Deg, ..) => { ..
Value::Dimension(n.$name_deg(), Unit::None, true) }) => Value::Dimension(SassNumber {
} num: Number(coerce_to_rad(num.0, unit).$name()),
v @ Value::Dimension(Some(..), ..) => { unit: Unit::None,
as_slash: None,
}),
v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to be an angle.", "$number: Expected {} to have an angle unit (deg, grad, rad, turn).",
v.inspect(args.span())? v.inspect(args.span())?
), ),
args.span(), args.span(),
) )
.into()) .into())
} }
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -353,27 +394,31 @@ macro_rules! trig_fn {
}; };
} }
trig_fn!(cos, cos_deg); trig_fn!(cos);
trig_fn!(sin, sin_deg); trig_fn!(sin);
trig_fn!(tan, tan_deg); trig_fn!(tan);
fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?; let number = args.get_err(0, "number")?;
Ok(match number { Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension( Value::Dimension(SassNumber {
if n > Number::from(1) || n < Number::from(-1) { num,
None unit: Unit::None,
} else if n.is_one() { ..
Some(Number::zero()) }) => Value::Dimension(SassNumber {
num: if num > Number::from(1) || num < Number::from(-1) {
Number(f64::NAN)
} else if num.is_one() {
Number::zero()
} else { } else {
n.acos() num.acos()
}, },
Unit::Deg, unit: Unit::Deg,
true, as_slash: None,
), }),
v @ Value::Dimension(Some(..), ..) => { v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to be unitless.", "$number: Expected {} to be unitless.",
@ -383,7 +428,6 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -394,21 +438,37 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}) })
} }
fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?; let number = args.get_err(0, "number")?;
Ok(match number { Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => { Value::Dimension(SassNumber {
if n > Number::from(1) || n < Number::from(-1) { num,
return Ok(Value::Dimension(None, Unit::Deg, true)); unit: Unit::None,
} else if n.is_zero() { ..
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true)); }) => {
if num > Number::from(1) || num < Number::from(-1) {
return Ok(Value::Dimension(SassNumber {
num: Number(f64::NAN),
unit: Unit::Deg,
as_slash: None,
}));
} else if num.is_zero() {
return Ok(Value::Dimension(SassNumber {
num: Number::zero(),
unit: Unit::Deg,
as_slash: None,
}));
} }
Value::Dimension(n.asin(), Unit::Deg, true) Value::Dimension(SassNumber {
num: num.asin(),
unit: Unit::Deg,
as_slash: None,
})
} }
v @ Value::Dimension(Some(..), ..) => { v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to be unitless.", "$number: Expected {} to be unitless.",
@ -418,7 +478,6 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -429,19 +488,31 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}) })
} }
fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?; let number = args.get_err(0, "number")?;
Ok(match number { Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => { Value::Dimension(SassNumber {
num: n,
unit: Unit::None,
..
}) => {
if n.is_zero() { if n.is_zero() {
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true)); return Ok(Value::Dimension(SassNumber {
num: (Number::zero()),
unit: Unit::Deg,
as_slash: None,
}));
} }
Value::Dimension(n.atan(), Unit::Deg, true) Value::Dimension(SassNumber {
num: n.atan(),
unit: Unit::Deg,
as_slash: None,
})
} }
v @ Value::Dimension(Some(..), ..) => { v @ Value::Dimension(SassNumber { .. }) => {
return Err(( return Err((
format!( format!(
"$number: Expected {} to be unitless.", "$number: Expected {} to be unitless.",
@ -451,7 +522,6 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()) .into())
} }
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
v => { v => {
return Err(( return Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
@ -462,10 +532,12 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}) })
} }
fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> { fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let (y_num, y_unit) = match args.get_err(0, "y")? { let (y_num, y_unit) = match args.get_err(0, "y")? {
Value::Dimension(n, u, ..) => (n, u), Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => (n, u),
v => { v => {
return Err(( return Err((
format!("$y: {} is not a number.", v.inspect(args.span())?), format!("$y: {} is not a number.", v.inspect(args.span())?),
@ -476,7 +548,9 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}; };
let (x_num, x_unit) = match args.get_err(1, "x")? { let (x_num, x_unit) = match args.get_err(1, "x")? {
Value::Dimension(n, u, ..) => (n, u), Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => (n, u),
v => { v => {
return Err(( return Err((
format!("$x: {} is not a number.", v.inspect(args.span())?), format!("$x: {} is not a number.", v.inspect(args.span())?),
@ -487,17 +561,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
}; };
let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None { let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None {
let x = match x_num { (x_num, y_num)
Some(n) => n,
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
};
let y = match y_num {
Some(n) => n,
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
};
(x, y)
} else if y_unit == Unit::None { } else if y_unit == Unit::None {
return Err(( return Err((
format!( format!(
@ -519,17 +583,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
) )
.into()); .into());
} else if x_unit.comparable(&y_unit) { } else if x_unit.comparable(&y_unit) {
let x = match x_num { (x_num, y_num.convert(&y_unit, &x_unit))
Some(n) => n,
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
};
let y = match y_num {
Some(n) => n,
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
};
(x, y.convert(&y_unit, &x_unit))
} else { } else {
return Err(( return Err((
format!("Incompatible units {} and {}.", y_unit, x_unit), format!("Incompatible units {} and {}.", y_unit, x_unit),
@ -538,51 +592,11 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
.into()); .into());
}; };
Ok( Ok(Value::Dimension(SassNumber {
match ( num: Number(y_num.0.atan2(x_num.0).to_degrees()),
NumberState::from_number(&x_num), unit: Unit::Deg,
NumberState::from_number(&y_num), as_slash: None,
) { }))
(NumberState::Zero, NumberState::FiniteNegative) => {
Value::Dimension(Some(Number::from(-90)), Unit::Deg, true)
}
(NumberState::Zero, NumberState::Zero) | (NumberState::Finite, NumberState::Zero) => {
Value::Dimension(Some(Number::zero()), Unit::Deg, true)
}
(NumberState::Zero, NumberState::Finite) => {
Value::Dimension(Some(Number::from(90)), Unit::Deg, true)
}
(NumberState::Finite, NumberState::Finite)
| (NumberState::FiniteNegative, NumberState::Finite)
| (NumberState::Finite, NumberState::FiniteNegative)
| (NumberState::FiniteNegative, NumberState::FiniteNegative) => Value::Dimension(
y_num
.atan2(x_num)
.map(|n| (n * Number::from(180)) / Number::pi()),
Unit::Deg,
true,
),
(NumberState::FiniteNegative, NumberState::Zero) => {
Value::Dimension(Some(Number::from(180)), Unit::Deg, true)
}
},
)
}
enum NumberState {
Zero,
Finite,
FiniteNegative,
}
impl NumberState {
fn from_number(num: &Number) -> Self {
match (num.is_zero(), num.is_positive()) {
(true, _) => NumberState::Zero,
(false, true) => NumberState::Finite,
(false, false) => NumberState::FiniteNegative,
}
}
} }
pub(crate) fn declare(f: &mut Module) { pub(crate) fn declare(f: &mut Module) {
@ -614,10 +628,58 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin_var( f.insert_builtin_var(
"e", "e",
Value::Dimension(Some(Number::from(std::f64::consts::E)), Unit::None, true), Value::Dimension(SassNumber {
num: Number::from(std::f64::consts::E),
unit: Unit::None,
as_slash: None,
}),
); );
f.insert_builtin_var( f.insert_builtin_var(
"pi", "pi",
Value::Dimension(Some(Number::from(std::f64::consts::PI)), Unit::None, true), Value::Dimension(SassNumber {
num: Number::from(std::f64::consts::PI),
unit: Unit::None,
as_slash: None,
}),
);
f.insert_builtin_var(
"epsilon",
Value::Dimension(SassNumber {
num: Number::from(std::f64::EPSILON),
unit: Unit::None,
as_slash: None,
}),
);
f.insert_builtin_var(
"max-safe-integer",
Value::Dimension(SassNumber {
num: Number::from(9007199254740991.0),
unit: Unit::None,
as_slash: None,
}),
);
f.insert_builtin_var(
"min-safe-integer",
Value::Dimension(SassNumber {
num: Number::from(-9007199254740991.0),
unit: Unit::None,
as_slash: None,
}),
);
f.insert_builtin_var(
"max-number",
Value::Dimension(SassNumber {
num: Number::from(f64::MAX),
unit: Unit::None,
as_slash: None,
}),
);
f.insert_builtin_var(
"min-number",
Value::Dimension(SassNumber {
num: Number::from(f64::MIN_POSITIVE),
unit: Unit::None,
as_slash: None,
}),
); );
} }

View File

@ -1,20 +1,20 @@
use codemap::Spanned; use std::cell::RefCell;
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::{ use crate::ast::{Configuration, ConfiguredValue};
args::CallArgs, use crate::builtin::builtin_imports::*;
builtin::{
use crate::builtin::{
meta::{ meta::{
call, content_exists, feature_exists, function_exists, get_function, call, content_exists, feature_exists, function_exists, get_function,
global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists, global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists,
}, },
modules::{Module, ModuleConfig}, modules::Module,
},
error::SassResult,
parse::{Parser, Stmt},
value::Value,
}; };
use crate::serializer::serialize_calculation_arg;
fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> { fn load_css(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<()> {
args.max_args(2)?; args.max_args(2)?;
let span = args.span(); let span = args.span();
@ -30,19 +30,21 @@ fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
} }
}; };
let with = match args.default_arg(1, "with", Value::Null)? { let with = match args.default_arg(1, "with", Value::Null) {
Value::Map(map) => Some(map), Value::Map(map) => Some(map),
Value::Null => None, Value::Null => None,
v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()), v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()),
}; };
// todo: tests for `with` let mut configuration = Configuration::empty();
if let Some(with) = with {
let mut config = ModuleConfig::default();
if let Some(with) = with {
visitor.emit_warning("`grass` does not currently support the $with parameter of load-css. This file will be imported the same way it would using `@import`.", args.span());
let mut values = BTreeMap::new();
for (key, value) in with { for (key, value) in with {
let key = match key { let name = match key.node {
Value::String(s, ..) => s, Value::String(s, ..) => Identifier::from(s),
v => { v => {
return Err(( return Err((
format!("$with key: {} is not a string.", v.inspect(span)?), format!("$with key: {} is not a string.", v.inspect(span)?),
@ -52,24 +54,45 @@ fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
} }
}; };
config.insert( if values.contains_key(&name) {
Spanned { // todo: write test for this
node: key.into(), return Err((
span, format!("The variable {name} was configured twice."),
}, key.span,
value.span(span), )
)?; .into());
} }
let (_, stmts) = parser.load_module(&url, &mut config)?; values.insert(name, ConfiguredValue::explicit(value, args.span()));
Ok(stmts)
} else {
parser.parse_single_import(&url, span)
}
} }
fn module_functions(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { configuration = Configuration::explicit(values, args.span());
}
let _configuration = Arc::new(RefCell::new(configuration));
let style_sheet = visitor.load_style_sheet(url.as_ref(), false, args.span())?;
visitor.visit_stylesheet(style_sheet)?;
// todo: support the $with argument to load-css
// visitor.load_module(
// url.as_ref(),
// Some(Arc::clone(&configuration)),
// true,
// args.span(),
// |visitor, module, stylesheet| {
// // (*module).borrow()
// Ok(())
// },
// )?;
// Visitor::assert_configuration_is_empty(&configuration, true)?;
Ok(())
}
fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let module = match args.get_err(0, "module")? { let module = match args.get_err(0, "module")? {
@ -84,11 +107,15 @@ fn module_functions(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
}; };
Ok(Value::Map( Ok(Value::Map(
parser.modules.get(module.into(), args.span())?.functions(), (*(*visitor.env.modules)
.borrow()
.get(module.into(), args.span())?)
.borrow()
.functions(args.span()),
)) ))
} }
fn module_variables(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> { fn module_variables(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let module = match args.get_err(0, "module")? { let module = match args.get_err(0, "module")? {
@ -103,10 +130,66 @@ fn module_variables(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
}; };
Ok(Value::Map( Ok(Value::Map(
parser.modules.get(module.into(), args.span())?.variables(), (*(*visitor.env.modules)
.borrow()
.get(module.into(), args.span())?)
.borrow()
.variables(args.span()),
)) ))
} }
fn calc_args(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
let calc = match args.get_err(0, "calc")? {
Value::Calculation(calc) => calc,
v => {
return Err((
format!("$calc: {} is not a calculation.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let args = calc
.args
.into_iter()
.map(|arg| {
Ok(match arg {
CalculationArg::Number(num) => Value::Dimension(num),
CalculationArg::Calculation(calc) => Value::Calculation(calc),
CalculationArg::String(s) | CalculationArg::Interpolation(s) => {
Value::String(s, QuoteKind::None)
}
CalculationArg::Operation { .. } => Value::String(
serialize_calculation_arg(&arg, visitor.options, args.span())?,
QuoteKind::None,
),
})
})
.collect::<SassResult<Vec<_>>>()?;
Ok(Value::List(args, ListSeparator::Comma, Brackets::None))
}
fn calc_name(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
let calc = match args.get_err(0, "calc")? {
Value::Calculation(calc) => calc,
v => {
return Err((
format!("$calc: {} is not a calculation.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
Ok(Value::String(calc.name.to_string(), QuoteKind::Quoted))
}
pub(crate) fn declare(f: &mut Module) { pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("feature-exists", feature_exists); f.insert_builtin("feature-exists", feature_exists);
f.insert_builtin("inspect", inspect); f.insert_builtin("inspect", inspect);
@ -121,6 +204,8 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("module-functions", module_functions); f.insert_builtin("module-functions", module_functions);
f.insert_builtin("get-function", get_function); f.insert_builtin("get-function", get_function);
f.insert_builtin("call", call); f.insert_builtin("call", call);
f.insert_builtin("calc-args", calc_args);
f.insert_builtin("calc-name", calc_name);
f.insert_builtin_mixin("load-css", load_css); f.insert_builtin_mixin("load-css", load_css);
} }

View File

@ -1,18 +1,25 @@
use std::collections::BTreeMap; use std::{
cell::RefCell,
collections::{BTreeMap, HashSet},
fmt,
sync::Arc,
};
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
use crate::{ use crate::{
args::CallArgs, ast::{ArgumentResult, AstForwardRule, BuiltinMixin, Mixin},
atrule::mixin::{BuiltinMixin, Mixin},
builtin::Builtin, builtin::Builtin,
common::{Identifier, QuoteKind}, common::Identifier,
error::SassResult, error::SassResult,
parse::Parser, evaluate::{Environment, Visitor},
scope::Scope, selector::ExtensionStore,
utils::{BaseMapView, MapView, MergedMapView, PublicMemberMapView},
value::{SassFunction, SassMap, Value}, value::{SassFunction, SassMap, Value},
}; };
use super::builtin_imports::QuoteKind;
mod color; mod color;
mod list; mod list;
mod map; mod map;
@ -21,51 +28,89 @@ mod meta;
mod selector; mod selector;
mod string; mod string;
#[derive(Debug, Default)] #[derive(Debug, Clone)]
pub(crate) struct Module { pub(crate) struct ForwardedModule {
pub scope: Scope, inner: Arc<RefCell<Module>>,
#[allow(dead_code)]
/// A module can itself import other modules forward_rule: AstForwardRule,
pub modules: Modules,
/// Whether or not this module is builtin
/// e.g. `"sass:math"`
is_builtin: bool,
} }
#[derive(Debug, Default)] impl ForwardedModule {
pub(crate) struct Modules(BTreeMap<Identifier, Module>); pub fn if_necessary(
module: Arc<RefCell<Module>>,
#[derive(Debug, Default)] rule: AstForwardRule,
pub(crate) struct ModuleConfig(BTreeMap<Identifier, Value>); ) -> Arc<RefCell<Module>> {
if rule.prefix.is_none()
impl ModuleConfig { && rule.shown_mixins_and_functions.is_none()
/// Removes and returns element with name && rule.shown_variables.is_none()
pub fn get(&mut self, name: Identifier) -> Option<Value> { && rule
self.0.remove(&name) .hidden_mixins_and_functions
} .as_ref()
.map_or(false, HashSet::is_empty)
/// If this structure is not empty at the end of && rule
/// an `@use`, we must throw an error .hidden_variables
pub fn is_empty(&self) -> bool { .as_ref()
self.0.is_empty() .map_or(false, HashSet::is_empty)
} {
module
pub fn insert(&mut self, name: Spanned<Identifier>, value: Spanned<Value>) -> SassResult<()> {
if self.0.insert(name.node, value.node).is_some() {
Err((
"The same variable may only be configured once.",
name.span.merge(value.span),
)
.into())
} else { } else {
Ok(()) Arc::new(RefCell::new(Module::Forwarded(ForwardedModule {
inner: module,
forward_rule: rule,
})))
} }
} }
} }
#[derive(Debug, Clone)]
pub(crate) struct ModuleScope {
pub variables: Arc<dyn MapView<Value = Value>>,
pub mixins: Arc<dyn MapView<Value = Mixin>>,
pub functions: Arc<dyn MapView<Value = SassFunction>>,
}
impl ModuleScope {
pub fn new() -> Self {
Self {
variables: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
mixins: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
functions: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Module {
Environment {
scope: ModuleScope,
#[allow(dead_code)]
upstream: Vec<Module>,
#[allow(dead_code)]
extension_store: ExtensionStore,
#[allow(dead_code)]
env: Environment,
},
Builtin {
scope: ModuleScope,
},
Forwarded(ForwardedModule),
}
#[derive(Debug, Clone)]
pub(crate) struct Modules(BTreeMap<Identifier, Arc<RefCell<Module>>>);
impl Modules { impl Modules {
pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> { pub fn new() -> Self {
Self(BTreeMap::new())
}
pub fn insert(
&mut self,
name: Identifier,
module: Arc<RefCell<Module>>,
span: Span,
) -> SassResult<()> {
if self.0.contains_key(&name) { if self.0.contains_key(&name) {
return Err(( return Err((
format!("There's already a module with namespace \"{}\".", name), format!("There's already a module with namespace \"{}\".", name),
@ -79,9 +124,9 @@ impl Modules {
Ok(()) Ok(())
} }
pub fn get(&self, name: Identifier, span: Span) -> SassResult<&Module> { pub fn get(&self, name: Identifier, span: Span) -> SassResult<Arc<RefCell<Module>>> {
match self.0.get(&name) { match self.0.get(&name) {
Some(v) => Ok(v), Some(v) => Ok(Arc::clone(v)),
None => Err(( None => Err((
format!( format!(
"There is no module with the namespace \"{}\".", "There is no module with the namespace \"{}\".",
@ -93,7 +138,11 @@ impl Modules {
} }
} }
pub fn get_mut(&mut self, name: Identifier, span: Span) -> SassResult<&mut Module> { pub fn get_mut(
&mut self,
name: Identifier,
span: Span,
) -> SassResult<&mut Arc<RefCell<Module>>> {
match self.0.get_mut(&name) { match self.0.get_mut(&name) {
Some(v) => Ok(v), Some(v) => Ok(v),
None => Err(( None => Err((
@ -106,153 +155,217 @@ impl Modules {
.into()), .into()),
} }
} }
pub fn merge(&mut self, other: Self) {
self.0.extend(other.0);
} }
fn member_map<V: fmt::Debug + Clone + 'static>(
local: Arc<dyn MapView<Value = V>>,
others: Vec<Arc<dyn MapView<Value = V>>>,
) -> Arc<dyn MapView<Value = V>> {
let local_map = PublicMemberMapView(local);
if others.is_empty() {
return Arc::new(local_map);
}
let mut all_maps: Vec<Arc<dyn MapView<Value = V>>> =
others.into_iter().filter(|map| !map.is_empty()).collect();
all_maps.push(Arc::new(local_map));
// todo: potential optimization when all_maps.len() == 1
Arc::new(MergedMapView::new(all_maps))
} }
impl Module { impl Module {
pub fn new_env(env: Environment, extension_store: ExtensionStore) -> Self {
let variables = {
let variables = (*env.forwarded_modules).borrow();
let variables = variables
.iter()
.map(|module| Arc::clone(&(*module).borrow().scope().variables));
let this = Arc::new(BaseMapView(env.global_vars()));
member_map(this, variables.collect())
};
let mixins = {
let mixins = (*env.forwarded_modules).borrow();
let mixins = mixins
.iter()
.map(|module| Arc::clone(&(*module).borrow().scope().mixins));
let this = Arc::new(BaseMapView(env.global_mixins()));
member_map(this, mixins.collect())
};
let functions = {
let functions = (*env.forwarded_modules).borrow();
let functions = functions
.iter()
.map(|module| Arc::clone(&(*module).borrow().scope().functions));
let this = Arc::new(BaseMapView(env.global_functions()));
member_map(this, functions.collect())
};
let scope = ModuleScope {
variables,
mixins,
functions,
};
Module::Environment {
scope,
upstream: Vec::new(),
extension_store,
env,
}
}
pub fn new_builtin() -> Self { pub fn new_builtin() -> Self {
Module { Module::Builtin {
scope: Scope::default(), scope: ModuleScope::new(),
modules: Modules::default(),
is_builtin: true,
} }
} }
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> { fn scope(&self) -> ModuleScope {
if name.node.as_str().starts_with('-') { match self {
return Err(( Self::Builtin { scope } | Self::Environment { scope, .. } => scope.clone(),
"Private members can't be accessed from outside their modules.", Self::Forwarded(forwarded) => (*forwarded.inner).borrow().scope(),
name.span, }
)
.into());
} }
match self.scope.vars.get(&name.node) { pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<Value> {
let scope = self.scope();
match scope.variables.get(name.node) {
Some(v) => Ok(v), Some(v) => Ok(v),
None => Err(("Undefined variable.", name.span).into()), None => Err(("Undefined variable.", name.span).into()),
} }
} }
pub fn get_var_no_err(&self, name: Identifier) -> Option<Value> {
let scope = self.scope();
scope.variables.get(name)
}
pub fn get_mixin_no_err(&self, name: Identifier) -> Option<Mixin> {
let scope = self.scope();
scope.mixins.get(name)
}
pub fn update_var(&mut self, name: Spanned<Identifier>, value: Value) -> SassResult<()> { pub fn update_var(&mut self, name: Spanned<Identifier>, value: Value) -> SassResult<()> {
if self.is_builtin { let scope = match self {
return Err(("Cannot modify built-in variable.", name.span).into()); Self::Builtin { .. } => {
return Err(("Cannot modify built-in variable.", name.span).into())
}
Self::Environment { scope, .. } => scope.clone(),
Self::Forwarded(forwarded) => (*forwarded.inner).borrow_mut().scope(),
};
if scope.variables.insert(name.node, value).is_none() {
return Err(("Undefined variable.", name.span).into());
} }
if name.node.as_str().starts_with('-') {
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
}
if self.scope.insert_var(name.node, value).is_some() {
Ok(()) Ok(())
} else {
Err(("Undefined variable.", name.span).into())
}
} }
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> { pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
if name.node.as_str().starts_with('-') { let scope = self.scope();
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
}
match self.scope.mixins.get(&name.node) { match scope.mixins.get(name.node) {
Some(v) => Ok(v.clone()), Some(v) => Ok(v),
None => Err(("Undefined mixin.", name.span).into()), None => Err(("Undefined mixin.", name.span).into()),
} }
} }
pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) { pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) {
self.scope.mixins.insert(name.into(), Mixin::Builtin(mixin)); let scope = self.scope();
scope.mixins.insert(name.into(), Mixin::Builtin(mixin));
} }
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) { pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
self.scope.vars.insert(name.into(), value); let ident = name.into();
let scope = self.scope();
scope.variables.insert(ident, value);
} }
pub fn get_fn(&self, name: Spanned<Identifier>) -> SassResult<Option<SassFunction>> { pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
if name.node.as_str().starts_with('-') { let scope = self.scope();
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
}
Ok(self.scope.functions.get(&name.node).cloned()) scope.functions.get(name)
} }
pub fn var_exists(&self, name: Identifier) -> bool { pub fn var_exists(&self, name: Identifier) -> bool {
!name.as_str().starts_with('-') && self.scope.var_exists(name) let scope = self.scope();
scope.variables.get(name).is_some()
} }
pub fn mixin_exists(&self, name: Identifier) -> bool { pub fn mixin_exists(&self, name: Identifier) -> bool {
!name.as_str().starts_with('-') && self.scope.mixin_exists(name) let scope = self.scope();
scope.mixins.get(name).is_some()
} }
pub fn fn_exists(&self, name: Identifier) -> bool { pub fn fn_exists(&self, name: Identifier) -> bool {
!name.as_str().starts_with('-') && self.scope.fn_exists(name) let scope = self.scope();
scope.functions.get(name).is_some()
} }
pub fn insert_builtin( pub fn insert_builtin(
&mut self, &mut self,
name: &'static str, name: &'static str,
function: fn(CallArgs, &mut Parser) -> SassResult<Value>, function: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
) { ) {
let ident = name.into(); let ident = name.into();
self.scope
let scope = match self {
Self::Builtin { scope } => scope,
_ => unreachable!(),
};
scope
.functions .functions
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident)); .insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
} }
pub fn functions(&self) -> SassMap { pub fn functions(&self, span: Span) -> SassMap {
SassMap::new_with( SassMap::new_with(
self.scope self.scope()
.functions .functions
.iter() .iter()
.into_iter()
.filter(|(key, _)| !key.as_str().starts_with('-')) .filter(|(key, _)| !key.as_str().starts_with('-'))
.map(|(key, value)| { .map(|(key, value)| {
( (
Value::String(key.to_string(), QuoteKind::Quoted), Value::String(key.to_string(), QuoteKind::Quoted).span(span),
Value::FunctionRef(value.clone()), Value::FunctionRef(value),
) )
}) })
.collect::<Vec<(Value, Value)>>(), .collect::<Vec<_>>(),
) )
} }
pub fn variables(&self) -> SassMap { pub fn variables(&self, span: Span) -> SassMap {
SassMap::new_with( SassMap::new_with(
self.scope self.scope()
.vars .variables
.iter() .iter()
.into_iter()
.filter(|(key, _)| !key.as_str().starts_with('-')) .filter(|(key, _)| !key.as_str().starts_with('-'))
.map(|(key, value)| { .map(|(key, value)| {
( (
Value::String(key.to_string(), QuoteKind::Quoted), Value::String(key.to_string(), QuoteKind::Quoted).span(span),
value.clone(), value,
) )
}) })
.collect::<Vec<(Value, Value)>>(), .collect::<Vec<_>>(),
) )
} }
pub const fn new_from_scope(scope: Scope, modules: Modules, is_builtin: bool) -> Self {
Module {
scope,
modules,
is_builtin,
}
}
} }
pub(crate) fn declare_module_color() -> Module { pub(crate) fn declare_module_color() -> Module {

View File

@ -15,23 +15,28 @@
//! Named colors retain their original casing, //! Named colors retain their original casing,
//! so `rEd` should be emitted as `rEd`. //! so `rEd` should be emitted as `rEd`.
use std::{ use crate::value::{fuzzy_round, Number};
cmp::{max, min},
fmt::{self, Display},
};
use crate::value::Number;
pub(crate) use name::NAMED_COLORS; pub(crate) use name::NAMED_COLORS;
use num_traits::{One, Signed, ToPrimitive, Zero};
mod name; mod name;
// todo: only store alpha once on color
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Color { pub(crate) struct Color {
rgba: Rgba, rgba: Rgba,
hsla: Option<Hsla>, hsla: Option<Hsla>,
repr: String, pub format: ColorFormat,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum ColorFormat {
Rgb,
Hsl,
/// Literal string from source text. Either a named color like `red` or a hex color
// todo: make this is a span and lookup text from codemap
Literal(String),
/// Use the most appropriate format
Infer,
} }
impl PartialEq for Color { impl PartialEq for Color {
@ -48,12 +53,12 @@ impl Color {
green: Number, green: Number,
blue: Number, blue: Number,
alpha: Number, alpha: Number,
repr: String, format: ColorFormat,
) -> Color { ) -> Color {
Color { Color {
rgba: Rgba::new(red, green, blue, alpha), rgba: Rgba::new(red, green, blue, alpha),
hsla: None, hsla: None,
repr, format,
} }
} }
@ -63,12 +68,11 @@ impl Color {
blue: Number, blue: Number,
alpha: Number, alpha: Number,
hsla: Hsla, hsla: Hsla,
repr: String,
) -> Color { ) -> Color {
Color { Color {
rgba: Rgba::new(red, green, blue, alpha), rgba: Rgba::new(red, green, blue, alpha),
hsla: Some(hsla), hsla: Some(hsla),
repr, format: ColorFormat::Infer,
} }
} }
} }
@ -84,17 +88,17 @@ struct Rgba {
impl PartialEq for Rgba { impl PartialEq for Rgba {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
if self.red != other.red if self.red != other.red
&& !(self.red >= Number::from(255) && other.red >= Number::from(255)) && !(self.red >= Number::from(255.0) && other.red >= Number::from(255.0))
{ {
return false; return false;
} }
if self.green != other.green if self.green != other.green
&& !(self.green >= Number::from(255) && other.green >= Number::from(255)) && !(self.green >= Number::from(255.0) && other.green >= Number::from(255.0))
{ {
return false; return false;
} }
if self.blue != other.blue if self.blue != other.blue
&& !(self.blue >= Number::from(255) && other.blue >= Number::from(255)) && !(self.blue >= Number::from(255.0) && other.blue >= Number::from(255.0))
{ {
return false; return false;
} }
@ -120,7 +124,7 @@ impl Rgba {
} }
pub fn alpha(&self) -> Number { pub fn alpha(&self) -> Number {
self.alpha.clone() self.alpha
} }
} }
@ -143,29 +147,29 @@ impl Hsla {
} }
pub fn hue(&self) -> Number { pub fn hue(&self) -> Number {
self.hue.clone() self.hue
} }
pub fn saturation(&self) -> Number { pub fn saturation(&self) -> Number {
self.saturation.clone() self.saturation
} }
pub fn luminance(&self) -> Number { pub fn luminance(&self) -> Number {
self.luminance.clone() self.luminance
} }
pub fn alpha(&self) -> Number { pub fn alpha(&self) -> Number {
self.alpha.clone() self.alpha
} }
} }
// RGBA color functions // RGBA color functions
impl Color { impl Color {
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, repr: String) -> Self { pub fn new(red: u8, green: u8, blue: u8, alpha: u8, format: String) -> Self {
Color { Color {
rgba: Rgba::new(red.into(), green.into(), blue.into(), alpha.into()), rgba: Rgba::new(red.into(), green.into(), blue.into(), alpha.into()),
hsla: None, hsla: None,
repr, format: ColorFormat::Literal(format),
} }
} }
@ -177,50 +181,62 @@ impl Color {
mut blue: Number, mut blue: Number,
mut alpha: Number, mut alpha: Number,
) -> Self { ) -> Self {
red = red.clamp(0, 255); red = red.clamp(0.0, 255.0);
green = green.clamp(0, 255); green = green.clamp(0.0, 255.0);
blue = blue.clamp(0, 255); blue = blue.clamp(0.0, 255.0);
alpha = alpha.clamp(0, 1); alpha = alpha.clamp(0.0, 1.0);
let repr = repr(&red, &green, &blue, &alpha); Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
Color::new_rgba(red, green, blue, alpha, repr) }
pub fn from_rgba_fn(
mut red: Number,
mut green: Number,
mut blue: Number,
mut alpha: Number,
) -> Self {
red = red.clamp(0.0, 255.0);
green = green.clamp(0.0, 255.0);
blue = blue.clamp(0.0, 255.0);
alpha = alpha.clamp(0.0, 1.0);
Color::new_rgba(red, green, blue, alpha, ColorFormat::Rgb)
} }
pub fn red(&self) -> Number { pub fn red(&self) -> Number {
self.rgba.red.clone().round() self.rgba.red.round()
} }
pub fn blue(&self) -> Number { pub fn blue(&self) -> Number {
self.rgba.blue.clone().round() self.rgba.blue.round()
} }
pub fn green(&self) -> Number { pub fn green(&self) -> Number {
self.rgba.green.clone().round() self.rgba.green.round()
} }
/// Mix two colors together with weight /// Mix two colors together with weight
/// Algorithm adapted from /// Algorithm adapted from
/// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718> /// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718>
pub fn mix(self, other: &Color, weight: Number) -> Self { pub fn mix(self, other: &Color, weight: Number) -> Self {
let weight = weight.clamp(0, 100); let weight = weight.clamp(0.0, 100.0);
let normalized_weight = weight.clone() * Number::from(2) - Number::one(); let normalized_weight = weight * Number::from(2.0) - Number::one();
let alpha_distance = self.alpha() - other.alpha(); let alpha_distance = self.alpha() - other.alpha();
let combined_weight1 = let combined_weight1 = if normalized_weight * alpha_distance == Number::from(-1) {
if normalized_weight.clone() * alpha_distance.clone() == Number::from(-1) {
normalized_weight normalized_weight
} else { } else {
(normalized_weight.clone() + alpha_distance.clone()) (normalized_weight + alpha_distance)
/ (Number::one() + normalized_weight * alpha_distance) / (Number::one() + normalized_weight * alpha_distance)
}; };
let weight1 = (combined_weight1 + Number::one()) / Number::from(2); let weight1 = (combined_weight1 + Number::one()) / Number::from(2.0);
let weight2 = Number::one() - weight1.clone(); let weight2 = Number::one() - weight1;
Color::from_rgba( Color::from_rgba(
self.red() * weight1.clone() + other.red() * weight2.clone(), self.red() * weight1 + other.red() * weight2,
self.green() * weight1.clone() + other.green() * weight2.clone(), self.green() * weight1 + other.green() * weight2,
self.blue() * weight1 + other.blue() * weight2, self.blue() * weight1 + other.blue() * weight2,
self.alpha() * weight.clone() + other.alpha() * (Number::one() - weight), self.alpha() * weight + other.alpha() * (Number::one() - weight),
) )
} }
} }
@ -234,71 +250,71 @@ impl Color {
return h.hue(); return h.hue();
} }
let red = self.red() / Number::from(255); let red = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255); let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255); let blue = self.blue() / Number::from(255.0);
let min = min(&red, min(&green, &blue)).clone(); let min = red.min(green.min(blue));
let max = max(&red, max(&green, &blue)).clone(); let max = red.max(green.max(blue));
let delta = max.clone() - min.clone(); let delta = max - min;
let hue = if min == max { let hue = if min == max {
Number::zero() Number::zero()
} else if max == red { } else if max == red {
Number::from(60_u8) * (green - blue) / delta Number::from(60.0) * (green - blue) / delta
} else if max == green { } else if max == green {
Number::from(120_u8) + Number::from(60_u8) * (blue - red) / delta Number::from(120.0) + Number::from(60.0) * (blue - red) / delta
} else { } else {
Number::from(240_u8) + Number::from(60_u8) * (red - green) / delta Number::from(240.0) + Number::from(60.0) * (red - green) / delta
}; };
hue % Number::from(360) hue % Number::from(360.0)
} }
/// Calculate saturation from RGBA values /// Calculate saturation from RGBA values
pub fn saturation(&self) -> Number { pub fn saturation(&self) -> Number {
if let Some(h) = &self.hsla { if let Some(h) = &self.hsla {
return h.saturation() * Number::from(100); return h.saturation() * Number::from(100.0);
} }
let red: Number = self.red() / Number::from(255); let red: Number = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255); let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255); let blue = self.blue() / Number::from(255.0);
let min = min(&red, min(&green, &blue)).clone(); let min = red.min(green.min(blue));
let max = red.max(green.max(blue)); let max = red.max(green.max(blue));
if min == max { if min == max {
return Number::zero(); return Number::zero();
} }
let delta = max.clone() - min.clone(); let delta = max - min;
let sum = max + min; let sum = max + min;
let s = delta let s = delta
/ if sum > Number::one() { / if sum > Number::one() {
Number::from(2) - sum Number::from(2.0) - sum
} else { } else {
sum sum
}; };
s * Number::from(100) s * Number::from(100.0)
} }
/// Calculate luminance from RGBA values /// Calculate luminance from RGBA values
pub fn lightness(&self) -> Number { pub fn lightness(&self) -> Number {
if let Some(h) = &self.hsla { if let Some(h) = &self.hsla {
return h.luminance() * Number::from(100); return h.luminance() * Number::from(100.0);
} }
let red: Number = self.red() / Number::from(255); let red: Number = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255); let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255); let blue = self.blue() / Number::from(255.0);
let min = min(&red, min(&green, &blue)).clone(); let min = red.min(green.min(blue));
let max = red.max(green.max(blue)); let max = red.max(green.max(blue));
(((min + max) / Number::from(2)) * Number::from(100)).round() (((min + max) / Number::from(2.0)) * Number::from(100.0)).round()
} }
pub fn as_hsla(&self) -> (Number, Number, Number, Number) { pub fn as_hsla(&self) -> (Number, Number, Number, Number) {
@ -306,21 +322,21 @@ impl Color {
return (h.hue(), h.saturation(), h.luminance(), h.alpha()); return (h.hue(), h.saturation(), h.luminance(), h.alpha());
} }
let red = self.red() / Number::from(255); let red = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255); let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255); let blue = self.blue() / Number::from(255.0);
let min = min(&red, min(&green, &blue)).clone(); let min = red.min(green.min(blue));
let max = max(&red, max(&green, &blue)).clone(); let max = red.max(green.max(blue));
let lightness = (min.clone() + max.clone()) / Number::from(2); let lightness = (min + max) / Number::from(2.0);
let saturation = if min == max { let saturation = if min == max {
Number::zero() Number::zero()
} else { } else {
let d = max.clone() - min.clone(); let d = max - min;
let mm = max.clone() + min.clone(); let mm = max + min;
d / if mm > Number::one() { d / if mm > Number::one() {
Number::from(2) - mm Number::from(2.0) - mm
} else { } else {
mm mm
} }
@ -329,20 +345,20 @@ impl Color {
let mut hue = if min == max { let mut hue = if min == max {
Number::zero() Number::zero()
} else if blue == max { } else if blue == max {
Number::from(4) + (red - green) / (max - min) Number::from(4.0) + (red - green) / (max - min)
} else if green == max { } else if green == max {
Number::from(2) + (blue - red) / (max - min) Number::from(2.0) + (blue - red) / (max - min)
} else { } else {
(green - blue) / (max - min) (green - blue) / (max - min)
}; };
if hue.is_negative() { if hue.is_negative() {
hue += Number::from(360); hue += Number::from(360.0);
} }
hue *= Number::from(60); hue *= Number::from(60.0);
(hue, saturation, lightness, self.alpha()) (hue % Number(360.0), saturation, lightness, self.alpha())
} }
pub fn adjust_hue(&self, degrees: Number) -> Self { pub fn adjust_hue(&self, degrees: Number) -> Self {
@ -362,92 +378,67 @@ impl Color {
pub fn saturate(&self, amount: Number) -> Self { pub fn saturate(&self, amount: Number) -> Self {
let (hue, saturation, luminance, alpha) = self.as_hsla(); let (hue, saturation, luminance, alpha) = self.as_hsla();
Color::from_hsla(hue, saturation + amount, luminance, alpha) Color::from_hsla(hue, (saturation + amount).clamp(0.0, 1.0), luminance, alpha)
} }
pub fn desaturate(&self, amount: Number) -> Self { pub fn desaturate(&self, amount: Number) -> Self {
let (hue, saturation, luminance, alpha) = self.as_hsla(); let (hue, saturation, luminance, alpha) = self.as_hsla();
Color::from_hsla(hue, saturation - amount, luminance, alpha) Color::from_hsla(hue, (saturation - amount).clamp(0.0, 1.0), luminance, alpha)
}
pub fn from_hsla_fn(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self {
let mut color = Self::from_hsla(hue, saturation, luminance, alpha);
color.format = ColorFormat::Hsl;
color
} }
/// Create RGBA representation from HSLA values /// Create RGBA representation from HSLA values
pub fn from_hsla(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self { pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self {
let mut hue = if hue >= Number::from(360) {
hue % Number::from(360)
} else if hue < Number::from(-360) {
Number::from(360) + hue % Number::from(360)
} else if hue.is_negative() {
Number::from(360) + hue.clamp(-360, 360)
} else {
hue
};
let saturation = saturation.clamp(0, 1);
let luminance = luminance.clamp(0, 1);
let alpha = alpha.clamp(0, 1);
let hsla = Hsla::new( let hsla = Hsla::new(
hue.clone(), hue,
saturation.clone(), saturation.clamp(0.0, 1.0),
luminance.clone(), lightness.clamp(0.0, 1.0),
alpha.clone(), alpha,
); );
if saturation.is_zero() { let scaled_hue = hue.0 / 360.0;
let val = luminance * Number::from(255); let scaled_saturation = saturation.0.clamp(0.0, 1.0);
let repr = repr(&val, &val, &val, &alpha); let scaled_lightness = lightness.0.clamp(0.0, 1.0);
return Color::new_hsla(val.clone(), val.clone(), val, alpha, hsla, repr);
}
let temporary_1 = if luminance < Number::small_ratio(1, 2) { let m2 = if scaled_lightness <= 0.5 {
luminance.clone() * (Number::one() + saturation) scaled_lightness * (scaled_saturation + 1.0)
} else { } else {
luminance.clone() + saturation.clone() - luminance.clone() * saturation scaled_lightness.mul_add(-scaled_saturation, scaled_lightness + scaled_saturation)
}; };
let temporary_2 = Number::from(2) * luminance - temporary_1.clone();
hue /= Number::from(360);
let mut temporary_r = hue.clone() + Number::small_ratio(1, 3);
let mut temporary_g = hue.clone();
let mut temporary_b = hue - Number::small_ratio(1, 3);
macro_rules! clamp_temp { let m1 = scaled_lightness.mul_add(2.0, -m2);
($temp:ident) => {
if $temp > Number::one() { let red = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue + 1.0 / 3.0) * 255.0);
$temp -= Number::one(); let green = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue) * 255.0);
} else if $temp.is_negative() { let blue = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue - 1.0 / 3.0) * 255.0);
$temp += Number::one();
} Color::new_hsla(Number(red), Number(green), Number(blue), alpha, hsla)
};
} }
clamp_temp!(temporary_r); fn hue_to_rgb(m1: f64, m2: f64, mut hue: f64) -> f64 {
clamp_temp!(temporary_g); if hue < 0.0 {
clamp_temp!(temporary_b); hue += 1.0;
}
if hue > 1.0 {
hue -= 1.0;
}
fn channel(temp: Number, temp1: &Number, temp2: &Number) -> Number { if hue < 1.0 / 6.0 {
Number::from(255) ((m2 - m1) * hue).mul_add(6.0, m1)
* if Number::from(6) * temp.clone() < Number::one() { } else if hue < 1.0 / 2.0 {
temp2.clone() + (temp1.clone() - temp2.clone()) * Number::from(6) * temp m2
} else if Number::from(2) * temp.clone() < Number::one() { } else if hue < 2.0 / 3.0 {
temp1.clone() ((m2 - m1) * (2.0 / 3.0 - hue)).mul_add(6.0, m1)
} else if Number::from(3) * temp.clone() < Number::from(2) {
temp2.clone()
+ (temp1.clone() - temp2.clone())
* (Number::small_ratio(2, 3) - temp)
* Number::from(6)
} else { } else {
temp2.clone() m1
} }
} }
let red = channel(temporary_r, &temporary_1, &temporary_2);
let green = channel(temporary_g, &temporary_1, &temporary_2);
let blue = channel(temporary_b, &temporary_1, &temporary_2);
let repr = repr(&red, &green, &blue, &alpha);
Color::new_hsla(red, green, blue, alpha, hsla, repr)
}
pub fn invert(&self, weight: Number) -> Self { pub fn invert(&self, weight: Number) -> Self {
if weight.is_zero() { if weight.is_zero() {
return self.clone(); return self.clone();
@ -456,9 +447,8 @@ impl Color {
let red = Number::from(u8::max_value()) - self.red(); let red = Number::from(u8::max_value()) - self.red();
let green = Number::from(u8::max_value()) - self.green(); let green = Number::from(u8::max_value()) - self.green();
let blue = Number::from(u8::max_value()) - self.blue(); let blue = Number::from(u8::max_value()) - self.blue();
let repr = repr(&red, &green, &blue, &self.alpha());
let inverse = Color::new_rgba(red, green, blue, self.alpha(), repr); let inverse = Color::new_rgba(red, green, blue, self.alpha(), ColorFormat::Infer);
inverse.mix(self, weight) inverse.mix(self, weight)
} }
@ -475,7 +465,7 @@ impl Color {
pub fn alpha(&self) -> Number { pub fn alpha(&self) -> Number {
let a = self.rgba.alpha(); let a = self.rgba.alpha();
if a > Number::one() { if a > Number::one() {
a / Number::from(255) a / Number::from(255.0)
} else { } else {
a a
} }
@ -505,108 +495,41 @@ impl Color {
impl Color { impl Color {
pub fn to_ie_hex_str(&self) -> String { pub fn to_ie_hex_str(&self) -> String {
format!( format!(
"#{:X}{:X}{:X}{:X}", "#{:02X}{:02X}{:02X}{:02X}",
(self.alpha() * Number::from(255)).round().to_integer(), fuzzy_round(self.alpha().0 * 255.0) as u8,
self.red().to_integer(), self.red().0 as u8,
self.green().to_integer(), self.green().0 as u8,
self.blue().to_integer() self.blue().0 as u8
) )
} }
} }
/// HWB color functions /// HWB color functions
impl Color { impl Color {
pub fn from_hwb( pub fn from_hwb(hue: Number, white: Number, black: Number, mut alpha: Number) -> Color {
mut hue: Number, let hue = Number(hue.rem_euclid(360.0) / 360.0);
mut white: Number, let mut scaled_white = white.0 / 100.0;
mut black: Number, let mut scaled_black = black.0 / 100.0;
mut alpha: Number, alpha = alpha.clamp(0.0, 1.0);
) -> Color {
hue %= Number::from(360);
hue /= Number::from(360);
white /= Number::from(100);
black /= Number::from(100);
alpha = alpha.clamp(Number::zero(), Number::one());
let white_black_sum = white.clone() + black.clone(); let white_black_sum = scaled_white + scaled_black;
if white_black_sum > Number::one() { if white_black_sum > 1.0 {
white /= white_black_sum.clone(); scaled_white /= white_black_sum;
black /= white_black_sum; scaled_black /= white_black_sum;
} }
let factor = Number::one() - white.clone() - black; let factor = 1.0 - scaled_white - scaled_black;
fn channel(m1: Number, m2: Number, mut hue: Number) -> Number { let to_rgb = |hue: f64| -> Number {
if hue < Number::zero() { let channel = Self::hue_to_rgb(0.0, 1.0, hue).mul_add(factor, scaled_white);
hue += Number::one(); Number(fuzzy_round(channel * 255.0))
}
if hue > Number::one() {
hue -= Number::one();
}
if hue < Number::small_ratio(1, 6) {
m1.clone() + (m2 - m1) * hue * Number::from(6)
} else if hue < Number::small_ratio(1, 2) {
m2
} else if hue < Number::small_ratio(2, 3) {
m1.clone() + (m2 - m1) * (Number::small_ratio(2, 3) - hue) * Number::from(6)
} else {
m1
}
}
let to_rgb = |hue: Number| -> Number {
let channel =
channel(Number::zero(), Number::one(), hue) * factor.clone() + white.clone();
channel * Number::from(255)
}; };
let red = to_rgb(hue.clone() + Number::small_ratio(1, 3)); let red = to_rgb(hue.0 + 1.0 / 3.0);
let green = to_rgb(hue.clone()); let green = to_rgb(hue.0);
let blue = to_rgb(hue - Number::small_ratio(1, 3)); let blue = to_rgb(hue.0 - 1.0 / 3.0);
let repr = repr(&red, &green, &blue, &alpha); Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
Color::new_rgba(red, green, blue, alpha, repr)
}
}
/// Get the proper representation from RGBA values
fn repr(red: &Number, green: &Number, blue: &Number, alpha: &Number) -> String {
fn into_u8(channel: &Number) -> u8 {
if channel > &Number::from(255) {
255_u8
} else if channel.is_negative() {
0_u8
} else {
channel.round().to_integer().to_u8().unwrap_or(255)
}
}
let red_u8 = into_u8(red);
let green_u8 = into_u8(green);
let blue_u8 = into_u8(blue);
if alpha < &Number::one() {
format!(
"rgba({}, {}, {}, {})",
red_u8,
green_u8,
blue_u8,
// todo: is_compressed
alpha.inspect()
)
} else if let Some(c) = NAMED_COLORS.get_by_rgba([red_u8, green_u8, blue_u8]) {
(*c).to_owned()
} else {
format!("#{:0>2x}{:0>2x}{:0>2x}", red_u8, green_u8, blue_u8)
}
}
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.repr)
} }
} }

View File

@ -3,7 +3,16 @@ use std::fmt::{self, Display, Write};
use crate::interner::InternedString; use crate::interner::InternedString;
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Op { pub enum UnaryOp {
Plus,
Neg,
Div,
Not,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum BinaryOp {
SingleEq,
Equal, Equal,
NotEqual, NotEqual,
GreaterThan, GreaterThan,
@ -14,51 +23,43 @@ pub enum Op {
Minus, Minus,
Mul, Mul,
Div, Div,
// todo: maybe rename mod, since it is mod
Rem, Rem,
And, And,
Or, Or,
Not,
} }
impl Display for Op { impl BinaryOp {
pub fn precedence(self) -> u8 {
match self {
Self::SingleEq => 0,
Self::Or => 1,
Self::And => 2,
Self::Equal | Self::NotEqual => 3,
Self::GreaterThan | Self::GreaterThanEqual | Self::LessThan | Self::LessThanEqual => 4,
Self::Plus | Self::Minus => 5,
Self::Mul | Self::Div | Self::Rem => 6,
}
}
}
impl Display for BinaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Equal => write!(f, "=="), BinaryOp::SingleEq => write!(f, "="),
Self::NotEqual => write!(f, "!="), BinaryOp::Equal => write!(f, "=="),
Self::GreaterThanEqual => write!(f, ">="), BinaryOp::NotEqual => write!(f, "!="),
Self::LessThanEqual => write!(f, "<="), BinaryOp::GreaterThanEqual => write!(f, ">="),
Self::GreaterThan => write!(f, ">"), BinaryOp::LessThanEqual => write!(f, "<="),
Self::LessThan => write!(f, "<"), BinaryOp::GreaterThan => write!(f, ">"),
Self::Plus => write!(f, "+"), BinaryOp::LessThan => write!(f, "<"),
Self::Minus => write!(f, "-"), BinaryOp::Plus => write!(f, "+"),
Self::Mul => write!(f, "*"), BinaryOp::Minus => write!(f, "-"),
Self::Div => write!(f, "/"), BinaryOp::Mul => write!(f, "*"),
Self::Rem => write!(f, "%"), BinaryOp::Div => write!(f, "/"),
Self::And => write!(f, "and"), BinaryOp::Rem => write!(f, "%"),
Self::Or => write!(f, "or"), BinaryOp::And => write!(f, "and"),
Self::Not => write!(f, "not"), BinaryOp::Or => write!(f, "or"),
}
}
}
impl Op {
/// Get order of precedence for an operator
///
/// Higher numbers are evaluated first.
/// Do not rely on the number itself, but rather the size relative to other numbers
///
/// If precedence is equal, the leftmost operation is evaluated first
pub fn precedence(self) -> usize {
match self {
Self::And | Self::Or | Self::Not => 0,
Self::Equal
| Self::NotEqual
| Self::GreaterThan
| Self::GreaterThanEqual
| Self::LessThan
| Self::LessThanEqual => 1,
Self::Plus | Self::Minus => 2,
Self::Mul | Self::Div | Self::Rem => 3,
} }
} }
} }
@ -85,31 +86,47 @@ pub(crate) enum Brackets {
Bracketed, Bracketed,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, Eq)]
pub(crate) enum ListSeparator { pub(crate) enum ListSeparator {
Space, Space,
Comma, Comma,
Slash,
Undecided,
}
impl PartialEq for ListSeparator {
#[allow(clippy::match_like_matches_macro)]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Space | Self::Undecided, Self::Space | Self::Undecided) => true,
(Self::Comma, Self::Comma) => true,
_ => false,
}
}
} }
impl ListSeparator { impl ListSeparator {
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
match self { match self {
Self::Space => " ", Self::Space | Self::Undecided => " ",
Self::Comma => ", ", Self::Comma => ", ",
Self::Slash => " / ",
} }
} }
pub fn as_compressed_str(self) -> &'static str { pub fn as_compressed_str(self) -> &'static str {
match self { match self {
Self::Space => " ", Self::Space | Self::Undecided => " ",
Self::Comma => ",", Self::Comma => ",",
Self::Slash => "/",
} }
} }
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
match self { match self {
Self::Space => "space", Self::Space | Self::Undecided => "space",
Self::Comma => "comma", Self::Comma => "comma",
Self::Slash => "slash",
} }
} }
} }
@ -119,9 +136,17 @@ impl ListSeparator {
/// ///
/// This struct protects that invariant by normalizing all /// This struct protects that invariant by normalizing all
/// underscores into hypens. /// underscores into hypens.
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)] #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)]
pub(crate) struct Identifier(InternedString); pub(crate) struct Identifier(InternedString);
impl fmt::Debug for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Identifier")
.field(&self.0.to_string())
.finish()
}
}
impl Identifier { impl Identifier {
fn from_str(s: &str) -> Self { fn from_str(s: &str) -> Self {
if s.contains('_') { if s.contains('_') {
@ -130,6 +155,10 @@ impl Identifier {
Identifier(InternedString::get_or_intern(s)) Identifier(InternedString::get_or_intern(s))
} }
} }
pub fn is_public(&self) -> bool {
!self.as_str().starts_with('-')
}
} }
impl From<String> for Identifier { impl From<String> for Identifier {

112
src/context_flags.rs Normal file
View File

@ -0,0 +1,112 @@
use std::ops::{BitAnd, BitOr, BitOrAssign};
#[derive(Debug, Copy, Clone)]
pub(crate) struct ContextFlags(pub u16);
#[derive(Debug, Copy, Clone)]
pub(crate) struct ContextFlag(u16);
impl ContextFlags {
pub const IN_MIXIN: ContextFlag = ContextFlag(1);
pub const IN_FUNCTION: ContextFlag = ContextFlag(1 << 1);
pub const IN_CONTROL_FLOW: ContextFlag = ContextFlag(1 << 2);
pub const IN_KEYFRAMES: ContextFlag = ContextFlag(1 << 3);
pub const FOUND_CONTENT_RULE: ContextFlag = ContextFlag(1 << 4);
pub const IN_STYLE_RULE: ContextFlag = ContextFlag(1 << 5);
pub const IN_UNKNOWN_AT_RULE: ContextFlag = ContextFlag(1 << 6);
pub const IN_CONTENT_BLOCK: ContextFlag = ContextFlag(1 << 7);
pub const IS_USE_ALLOWED: ContextFlag = ContextFlag(1 << 8);
pub const IN_PARENS: ContextFlag = ContextFlag(1 << 9);
pub const AT_ROOT_EXCLUDING_STYLE_RULE: ContextFlag = ContextFlag(1 << 10);
pub const IN_SUPPORTS_DECLARATION: ContextFlag = ContextFlag(1 << 11);
pub const IN_SEMI_GLOBAL_SCOPE: ContextFlag = ContextFlag(1 << 12);
pub const fn empty() -> Self {
Self(0)
}
pub fn unset(&mut self, flag: ContextFlag) {
self.0 &= !flag.0;
}
pub fn set(&mut self, flag: ContextFlag, v: bool) {
if v {
self.0 |= flag.0;
} else {
self.unset(flag);
}
}
pub fn in_mixin(self) -> bool {
(self.0 & Self::IN_MIXIN) != 0
}
pub fn in_function(self) -> bool {
(self.0 & Self::IN_FUNCTION) != 0
}
pub fn in_control_flow(self) -> bool {
(self.0 & Self::IN_CONTROL_FLOW) != 0
}
pub fn in_keyframes(self) -> bool {
(self.0 & Self::IN_KEYFRAMES) != 0
}
pub fn in_style_rule(self) -> bool {
(self.0 & Self::IN_STYLE_RULE) != 0
}
pub fn in_unknown_at_rule(self) -> bool {
(self.0 & Self::IN_UNKNOWN_AT_RULE) != 0
}
pub fn in_content_block(self) -> bool {
(self.0 & Self::IN_CONTENT_BLOCK) != 0
}
pub fn in_parens(self) -> bool {
(self.0 & Self::IN_PARENS) != 0
}
pub fn at_root_excluding_style_rule(self) -> bool {
(self.0 & Self::AT_ROOT_EXCLUDING_STYLE_RULE) != 0
}
pub fn in_supports_declaration(self) -> bool {
(self.0 & Self::IN_SUPPORTS_DECLARATION) != 0
}
pub fn in_semi_global_scope(self) -> bool {
(self.0 & Self::IN_SEMI_GLOBAL_SCOPE) != 0
}
pub fn found_content_rule(self) -> bool {
(self.0 & Self::FOUND_CONTENT_RULE) != 0
}
pub fn is_use_allowed(self) -> bool {
(self.0 & Self::IS_USE_ALLOWED) != 0
}
}
impl BitAnd<ContextFlag> for u16 {
type Output = Self;
#[inline]
fn bitand(self, rhs: ContextFlag) -> Self::Output {
self & rhs.0
}
}
impl BitOr<ContextFlag> for ContextFlags {
type Output = Self;
fn bitor(self, rhs: ContextFlag) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign<ContextFlag> for ContextFlags {
fn bitor_assign(&mut self, rhs: ContextFlag) {
self.0 |= rhs.0;
}
}

View File

@ -58,7 +58,7 @@ impl SassError {
pub(crate) fn raw(self) -> (String, Span) { pub(crate) fn raw(self) -> (String, Span) {
match self.kind { match self.kind {
SassErrorKind::Raw(string, span) => (string, span), SassErrorKind::Raw(string, span) => (string, span),
e => todo!("unable to get raw of {:?}", e), e => unreachable!("unable to get raw of {:?}", e),
} }
} }
@ -113,15 +113,15 @@ impl Display for SassError {
loc, loc,
unicode, unicode,
} => (message, loc, *unicode), } => (message, loc, *unicode),
SassErrorKind::FromUtf8Error(s) => return writeln!(f, "Error: {}", s), SassErrorKind::FromUtf8Error(..) => return writeln!(f, "Error: Invalid UTF-8."),
SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s), SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s),
SassErrorKind::Raw(..) => unreachable!(), SassErrorKind::Raw(..) => unreachable!(),
}; };
let first_bar = if unicode { '╷' } else { '|' }; let first_bar = if unicode { '╷' } else { ',' };
let second_bar = if unicode { '│' } else { '|' }; let second_bar = if unicode { '│' } else { '|' };
let third_bar = if unicode { '│' } else { '|' }; let third_bar = if unicode { '│' } else { '|' };
let fourth_bar = if unicode { '╵' } else { '|' }; let fourth_bar = if unicode { '╵' } else { '\'' };
let line = loc.begin.line + 1; let line = loc.begin.line + 1;
let col = loc.begin.column + 1; let col = loc.begin.column + 1;
@ -148,7 +148,12 @@ impl Display for SassError {
.collect::<String>() .collect::<String>()
)?; )?;
writeln!(f, "{}{}", padding, fourth_bar)?; writeln!(f, "{}{}", padding, fourth_bar)?;
if unicode {
writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?; writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
} else {
writeln!(f, " {} {}:{} root stylesheet", loc.file.name(), line, col)?;
}
Ok(()) Ok(())
} }
} }

603
src/evaluate/bin_op.rs Normal file
View File

@ -0,0 +1,603 @@
#![allow(unused_variables)]
use std::cmp::Ordering;
use codemap::Span;
use crate::{
common::{BinaryOp, QuoteKind},
error::SassResult,
serializer::serialize_number,
unit::Unit,
value::{SassNumber, Value},
Options,
};
pub(crate) fn add(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
Ok(match left {
Value::Calculation(..) => match right {
Value::String(s, quotes) => Value::String(
format!(
"{}{}",
left.to_css_string(span, options.is_compressed())?,
s
),
quotes,
),
_ => {
return Err((
format!(
"Undefined operation \"{} + {}\".",
left.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
},
Value::Map(..) | Value::FunctionRef(..) => {
return Err((
format!("{} isn't a valid CSS value.", left.inspect(span)?),
span,
)
.into())
}
Value::True | Value::False => match right {
Value::String(s, QuoteKind::Quoted) => Value::String(
format!(
"{}{}",
left.to_css_string(span, options.is_compressed())?,
s
),
QuoteKind::Quoted,
),
_ => Value::String(
format!(
"{}{}",
left.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
},
Value::Null => match right {
Value::Null => Value::Null,
_ => Value::String(
right
.to_css_string(span, options.is_compressed())?
.into_owned(),
QuoteKind::None,
),
},
Value::Dimension(SassNumber {
num,
unit,
as_slash: _,
}) => match right {
Value::Dimension(SassNumber {
num: num2,
unit: unit2,
as_slash: _,
}) => {
if !unit.comparable(&unit2) {
return Err(
(format!("Incompatible units {} and {}.", unit2, unit), span).into(),
);
}
if unit == unit2 {
Value::Dimension(SassNumber {
num: num + num2,
unit,
as_slash: None,
})
} else if unit == Unit::None {
Value::Dimension(SassNumber {
num: num + num2,
unit: unit2,
as_slash: None,
})
} else if unit2 == Unit::None {
Value::Dimension(SassNumber {
num: num + num2,
unit,
as_slash: None,
})
} else {
Value::Dimension(SassNumber {
num: num + num2.convert(&unit2, &unit),
unit,
as_slash: None,
})
}
}
Value::String(s, q) => Value::String(
format!("{}{}{}", num.to_string(options.is_compressed()), unit, s),
q,
),
Value::Null => Value::String(
format!("{}{}", num.to_string(options.is_compressed()), unit),
QuoteKind::None,
),
Value::True | Value::False | Value::List(..) | Value::ArgList(..) => Value::String(
format!(
"{}{}{}",
num.to_string(options.is_compressed()),
unit,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
Value::Map(..) | Value::FunctionRef(..) => {
return Err((
format!("{} isn't a valid CSS value.", right.inspect(span)?),
span,
)
.into())
}
Value::Color(..) | Value::Calculation(..) => {
return Err((
format!(
"Undefined operation \"{}{} + {}\".",
num.inspect(),
unit,
right.inspect(span)?
),
span,
)
.into())
}
},
c @ Value::Color(..) => match right {
// todo: we really cant add to any other types?
Value::String(..) | Value::Null | Value::List(..) => Value::String(
format!(
"{}{}",
c.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?,
),
QuoteKind::None,
),
_ => {
return Err((
format!(
"Undefined operation \"{} + {}\".",
c.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
},
Value::String(text, quotes) => match right {
Value::String(text2, ..) => Value::String(text + &text2, quotes),
_ => Value::String(
text + &right.to_css_string(span, options.is_compressed())?,
quotes,
),
},
Value::List(..) | Value::ArgList(..) => match right {
Value::String(s, q) => Value::String(
format!(
"{}{}",
left.to_css_string(span, options.is_compressed())?,
s
),
q,
),
_ => Value::String(
format!(
"{}{}",
left.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
},
})
}
pub(crate) fn sub(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
Ok(match left {
Value::Calculation(..) => {
return Err((
format!(
"Undefined operation \"{} - {}\".",
left.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
Value::Null => Value::String(
format!("-{}", right.to_css_string(span, options.is_compressed())?),
QuoteKind::None,
),
Value::Dimension(SassNumber {
num,
unit,
as_slash: _,
}) => match right {
Value::Dimension(SassNumber {
num: num2,
unit: unit2,
as_slash: _,
}) => {
if !unit.comparable(&unit2) {
return Err(
(format!("Incompatible units {} and {}.", unit2, unit), span).into(),
);
}
if unit == unit2 {
Value::Dimension(SassNumber {
num: num - num2,
unit,
as_slash: None,
})
} else if unit == Unit::None {
Value::Dimension(SassNumber {
num: num - num2,
unit: unit2,
as_slash: None,
})
} else if unit2 == Unit::None {
Value::Dimension(SassNumber {
num: num - num2,
unit,
as_slash: None,
})
} else {
Value::Dimension(SassNumber {
num: num - num2.convert(&unit2, &unit),
unit,
as_slash: None,
})
}
}
Value::List(..)
| Value::String(..)
| Value::True
| Value::False
| Value::ArgList(..) => Value::String(
format!(
"{}{}-{}",
num.to_string(options.is_compressed()),
unit,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
Value::Map(..) | Value::FunctionRef(..) => {
return Err((
format!("{} isn't a valid CSS value.", right.inspect(span)?),
span,
)
.into())
}
Value::Color(..) | Value::Calculation(..) => {
return Err((
format!(
"Undefined operation \"{}{} - {}\".",
num.inspect(),
unit,
right.inspect(span)?
),
span,
)
.into())
}
Value::Null => Value::String(
format!("{}{}-", num.to_string(options.is_compressed()), unit),
QuoteKind::None,
),
},
c @ Value::Color(..) => match right {
Value::Dimension(SassNumber { .. }) | Value::Color(..) => {
return Err((
format!(
"Undefined operation \"{} - {}\".",
c.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
_ => Value::String(
format!(
"{}-{}",
c.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
},
Value::String(..) => Value::String(
format!(
"{}-{}",
left.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
// todo: can be greatly simplified
_ => match right {
Value::String(s, q) => Value::String(
format!(
"{}-{}{}{}",
left.to_css_string(span, options.is_compressed())?,
q,
s,
q
),
QuoteKind::None,
),
Value::Null => Value::String(
format!("{}-", left.to_css_string(span, options.is_compressed())?),
QuoteKind::None,
),
_ => Value::String(
format!(
"{}-{}",
left.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
},
})
}
pub(crate) fn mul(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
Ok(match left {
Value::Dimension(SassNumber {
num,
unit,
as_slash: _,
}) => match right {
Value::Dimension(SassNumber {
num: num2,
unit: unit2,
as_slash: _,
}) => {
if unit2 == Unit::None {
return Ok(Value::Dimension(SassNumber {
num: num * num2,
unit,
as_slash: None,
}));
}
let n = SassNumber {
num,
unit,
as_slash: None,
} * SassNumber {
num: num2,
unit: unit2,
as_slash: None,
};
Value::Dimension(n)
}
_ => {
return Err((
format!(
"Undefined operation \"{}{} * {}\".",
num.inspect(),
unit,
right.inspect(span)?
),
span,
)
.into())
}
},
_ => {
return Err((
format!(
"Undefined operation \"{} * {}\".",
left.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
})
}
pub(crate) fn cmp(
left: &Value,
right: &Value,
options: &Options,
span: Span,
op: BinaryOp,
) -> SassResult<Value> {
let ordering = match left.cmp(right, span, op)? {
Some(ord) => ord,
None => return Ok(Value::False),
};
Ok(match op {
BinaryOp::GreaterThan => match ordering {
Ordering::Greater => Value::True,
Ordering::Less | Ordering::Equal => Value::False,
},
BinaryOp::GreaterThanEqual => match ordering {
Ordering::Greater | Ordering::Equal => Value::True,
Ordering::Less => Value::False,
},
BinaryOp::LessThan => match ordering {
Ordering::Less => Value::True,
Ordering::Greater | Ordering::Equal => Value::False,
},
BinaryOp::LessThanEqual => match ordering {
Ordering::Less | Ordering::Equal => Value::True,
Ordering::Greater => Value::False,
},
_ => unreachable!(),
})
}
pub(crate) fn single_eq(
left: &Value,
right: &Value,
options: &Options,
span: Span,
) -> SassResult<Value> {
Ok(Value::String(
format!(
"{}={}",
left.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
))
}
// todo: simplify matching
pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
Ok(match left {
Value::Dimension(SassNumber {
num,
unit,
as_slash: as_slash1,
}) => match right {
Value::Dimension(SassNumber {
num: num2,
unit: unit2,
as_slash: as_slash2,
}) => {
if unit2 == Unit::None {
return Ok(Value::Dimension(SassNumber {
num: num / num2,
unit,
as_slash: None,
}));
}
let n = SassNumber {
num,
unit,
as_slash: None,
} / SassNumber {
num: num2,
unit: unit2,
as_slash: None,
};
Value::Dimension(n)
}
_ => Value::String(
format!(
"{}/{}",
serialize_number(
&SassNumber {
num,
unit,
as_slash: as_slash1
},
options,
span
)?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
},
c @ Value::Color(..) => match right {
Value::Dimension(SassNumber { .. }) | Value::Color(..) => {
return Err((
format!(
"Undefined operation \"{} / {}\".",
c.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
_ => Value::String(
format!(
"{}/{}",
c.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
},
_ => Value::String(
format!(
"{}/{}",
left.to_css_string(span, options.is_compressed())?,
right.to_css_string(span, options.is_compressed())?
),
QuoteKind::None,
),
})
}
pub(crate) fn rem(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
Ok(match left {
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => match right {
Value::Dimension(SassNumber {
num: n2,
unit: u2,
as_slash: _,
}) => {
if !u.comparable(&u2) {
return Err((format!("Incompatible units {} and {}.", u, u2), span).into());
}
let new_num = n % n2.convert(&u2, &u);
let new_unit = if u == u2 {
u
} else if u == Unit::None {
u2
} else {
u
};
Value::Dimension(SassNumber {
num: new_num,
unit: new_unit,
as_slash: None,
})
}
_ => {
let val = Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: None,
})
.inspect(span)?;
return Err((
format!(
"Undefined operation \"{} % {}\".",
val,
right.inspect(span)?
),
span,
)
.into());
}
},
_ => {
return Err((
format!(
"Undefined operation \"{} % {}\".",
left.inspect(span)?,
right.inspect(span)?
),
span,
)
.into())
}
})
}

154
src/evaluate/css_tree.rs Normal file
View File

@ -0,0 +1,154 @@
use std::{
cell::{Ref, RefCell, RefMut},
collections::BTreeMap,
};
use crate::ast::CssStmt;
#[derive(Debug, Clone)]
pub(super) struct CssTree {
// None is tombstone
stmts: Vec<RefCell<Option<CssStmt>>>,
pub parent_to_child: BTreeMap<CssTreeIdx, Vec<CssTreeIdx>>,
pub child_to_parent: BTreeMap<CssTreeIdx, CssTreeIdx>,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
#[repr(transparent)]
pub(super) struct CssTreeIdx(usize);
impl CssTree {
pub const ROOT: CssTreeIdx = CssTreeIdx(0);
pub fn new() -> Self {
let mut tree = Self {
stmts: Vec::new(),
parent_to_child: BTreeMap::new(),
child_to_parent: BTreeMap::new(),
};
tree.stmts.push(RefCell::new(None));
tree
}
pub fn get(&self, idx: CssTreeIdx) -> Ref<Option<CssStmt>> {
self.stmts[idx.0].borrow()
}
pub fn get_mut(&self, idx: CssTreeIdx) -> RefMut<Option<CssStmt>> {
self.stmts[idx.0].borrow_mut()
}
pub fn finish(self) -> Vec<CssStmt> {
let mut idx = 1;
while idx < self.stmts.len() - 1 {
if self.stmts[idx].borrow().is_none() || !self.has_children(CssTreeIdx(idx)) {
idx += 1;
continue;
}
self.apply_children(CssTreeIdx(idx));
idx += 1;
}
self.stmts
.into_iter()
.filter_map(RefCell::into_inner)
.collect()
}
fn apply_children(&self, parent: CssTreeIdx) {
for &child in &self.parent_to_child[&parent] {
if self.has_children(child) {
self.apply_children(child);
}
match self.stmts[child.0].borrow_mut().take() {
Some(child) => self.add_child_to_parent(child, parent),
None => continue,
};
}
}
fn has_children(&self, parent: CssTreeIdx) -> bool {
self.parent_to_child.contains_key(&parent)
}
fn add_child_to_parent(&self, child: CssStmt, parent_idx: CssTreeIdx) {
let mut parent = self.stmts[parent_idx.0].borrow_mut().take();
match &mut parent {
Some(CssStmt::RuleSet { body, .. }) => body.push(child),
Some(CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..)) | None => {
unreachable!()
}
Some(CssStmt::Media(media, ..)) => {
media.body.push(child);
}
Some(CssStmt::UnknownAtRule(at_rule, ..)) => {
at_rule.body.push(child);
}
Some(CssStmt::Supports(supports, ..)) => {
supports.body.push(child);
}
Some(CssStmt::KeyframesRuleSet(keyframes)) => {
keyframes.body.push(child);
}
}
self.stmts[parent_idx.0]
.borrow_mut()
.replace(parent.unwrap());
}
pub fn add_child(&mut self, child: CssStmt, parent_idx: CssTreeIdx) -> CssTreeIdx {
let child_idx = self.add_stmt_inner(child);
self.parent_to_child
.entry(parent_idx)
.or_default()
.push(child_idx);
self.child_to_parent.insert(child_idx, parent_idx);
child_idx
}
pub fn link_child_to_parent(&mut self, child_idx: CssTreeIdx, parent_idx: CssTreeIdx) {
self.parent_to_child
.entry(parent_idx)
.or_default()
.push(child_idx);
self.child_to_parent.insert(child_idx, parent_idx);
}
pub fn has_following_sibling(&self, child: CssTreeIdx) -> bool {
if child == Self::ROOT {
return false;
}
let parent_idx = self.child_to_parent.get(&child).unwrap();
let parent_children = self.parent_to_child.get(parent_idx).unwrap();
let child_pos = parent_children
.iter()
.position(|child_idx| *child_idx == child)
.unwrap();
// todo: parent_children[child_pos + 1..] !is_invisible
child_pos + 1 < parent_children.len()
}
pub fn add_stmt(&mut self, child: CssStmt, parent: Option<CssTreeIdx>) -> CssTreeIdx {
match parent {
Some(parent) => self.add_child(child, parent),
None => self.add_child(child, Self::ROOT),
}
}
fn add_stmt_inner(&mut self, stmt: CssStmt) -> CssTreeIdx {
let idx = CssTreeIdx(self.stmts.len());
self.stmts.push(RefCell::new(Some(stmt)));
idx
}
}

281
src/evaluate/env.rs Normal file
View File

@ -0,0 +1,281 @@
use codemap::{Span, Spanned};
use crate::{
ast::{AstForwardRule, Mixin},
builtin::modules::{ForwardedModule, Module, Modules},
common::Identifier,
error::SassResult,
selector::ExtensionStore,
value::{SassFunction, Value},
};
use std::{cell::RefCell, collections::BTreeMap, sync::Arc};
use super::{scope::Scopes, visitor::CallableContentBlock};
#[derive(Debug, Clone)]
pub(crate) struct Environment {
pub scopes: Scopes,
pub modules: Arc<RefCell<Modules>>,
pub global_modules: Vec<Arc<RefCell<Module>>>,
pub content: Option<Arc<CallableContentBlock>>,
pub forwarded_modules: Arc<RefCell<Vec<Arc<RefCell<Module>>>>>,
}
impl Environment {
pub fn new() -> Self {
Self {
scopes: Scopes::new(),
modules: Arc::new(RefCell::new(Modules::new())),
global_modules: Vec::new(),
content: None,
forwarded_modules: Arc::new(RefCell::new(Vec::new())),
}
}
pub fn new_closure(&self) -> Self {
Self {
scopes: self.scopes.new_closure(),
modules: Arc::clone(&self.modules),
global_modules: self.global_modules.iter().map(Arc::clone).collect(),
content: self.content.as_ref().map(Arc::clone),
forwarded_modules: Arc::clone(&self.forwarded_modules),
}
}
pub fn forward_module(&mut self, module: Arc<RefCell<Module>>, rule: AstForwardRule) {
let view = ForwardedModule::if_necessary(module, rule);
(*self.forwarded_modules).borrow_mut().push(view);
// todo: assertnoconflicts
}
pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) {
self.scopes.insert_mixin(name, mixin);
}
pub fn mixin_exists(&self, name: Identifier) -> bool {
self.scopes.mixin_exists(name)
}
pub fn get_mixin(
&self,
name: Spanned<Identifier>,
namespace: Option<Spanned<Identifier>>,
) -> SassResult<Mixin> {
if let Some(namespace) = namespace {
let modules = (*self.modules).borrow();
let module = modules.get(namespace.node, namespace.span)?;
return (*module).borrow().get_mixin(name);
}
match self.scopes.get_mixin(name) {
Ok(v) => Ok(v),
Err(e) => {
if let Some(v) = self.get_mixin_from_global_modules(name.node) {
return Ok(v);
}
Err(e)
}
}
}
pub fn insert_fn(&mut self, func: SassFunction) {
self.scopes.insert_fn(func);
}
pub fn fn_exists(&self, name: Identifier) -> bool {
self.scopes.fn_exists(name)
}
pub fn get_fn(
&self,
name: Identifier,
namespace: Option<Spanned<Identifier>>,
) -> SassResult<Option<SassFunction>> {
if let Some(namespace) = namespace {
let modules = (*self.modules).borrow();
let module = modules.get(namespace.node, namespace.span)?;
return Ok((*module).borrow().get_fn(name));
}
Ok(self
.scopes
.get_fn(name)
.or_else(|| self.get_function_from_global_modules(name)))
}
pub fn var_exists(
&self,
name: Identifier,
namespace: Option<Spanned<Identifier>>,
) -> SassResult<bool> {
if let Some(namespace) = namespace {
let modules = (*self.modules).borrow();
let module = modules.get(namespace.node, namespace.span)?;
return Ok((*module).borrow().var_exists(name));
}
Ok(self.scopes.var_exists(name))
}
pub fn get_var(
&self,
name: Spanned<Identifier>,
namespace: Option<Spanned<Identifier>>,
) -> SassResult<Value> {
if let Some(namespace) = namespace {
let modules = (*self.modules).borrow();
let module = modules.get(namespace.node, namespace.span)?;
return (*module).borrow().get_var(name);
}
match self.scopes.get_var(name) {
Ok(v) => Ok(v),
Err(e) => {
if let Some(v) = self.get_variable_from_global_modules(name.node) {
return Ok(v);
}
Err(e)
}
}
}
pub fn insert_var(
&mut self,
name: Spanned<Identifier>,
namespace: Option<Spanned<Identifier>>,
value: Value,
is_global: bool,
in_semi_global_scope: bool,
) -> SassResult<()> {
if let Some(namespace) = namespace {
let mut modules = (*self.modules).borrow_mut();
let module = modules.get_mut(namespace.node, namespace.span)?;
(*module).borrow_mut().update_var(name, value)?;
return Ok(());
}
if is_global || self.at_root() {
// // Don't set the index if there's already a variable with the given name,
// // since local accesses should still return the local variable.
// _variableIndices.putIfAbsent(name, () {
// _lastVariableName = name;
// _lastVariableIndex = 0;
// return 0;
// });
// // If this module doesn't already contain a variable named [name], try
// // setting it in a global module.
// if (!_variables.first.containsKey(name)) {
// var moduleWithName = _fromOneModule(name, "variable",
// (module) => module.variables.containsKey(name) ? module : null);
// if (moduleWithName != null) {
// moduleWithName.setVariable(name, value, nodeWithSpan);
// return;
// }
// }
self.scopes.insert_var(0, name.node, value);
return Ok(());
}
let mut index = self
.scopes
.find_var(name.node)
.unwrap_or(self.scopes.len() - 1);
if !in_semi_global_scope && index == 0 {
index = self.scopes.len() - 1;
}
self.scopes.insert_var(index, name.node, value);
Ok(())
}
pub fn at_root(&self) -> bool {
self.scopes.len() == 1
}
pub fn scopes_mut(&mut self) -> &mut Scopes {
&mut self.scopes
}
pub fn global_vars(&self) -> Arc<RefCell<BTreeMap<Identifier, Value>>> {
self.scopes.global_variables()
}
pub fn global_mixins(&self) -> Arc<RefCell<BTreeMap<Identifier, Mixin>>> {
self.scopes.global_mixins()
}
pub fn global_functions(&self) -> Arc<RefCell<BTreeMap<Identifier, SassFunction>>> {
self.scopes.global_functions()
}
fn get_variable_from_global_modules(&self, name: Identifier) -> Option<Value> {
for module in &self.global_modules {
if (**module).borrow().var_exists(name) {
return (**module).borrow().get_var_no_err(name);
}
}
None
}
fn get_function_from_global_modules(&self, name: Identifier) -> Option<SassFunction> {
for module in &self.global_modules {
if (**module).borrow().fn_exists(name) {
return (**module).borrow().get_fn(name);
}
}
None
}
fn get_mixin_from_global_modules(&self, name: Identifier) -> Option<Mixin> {
for module in &self.global_modules {
if (**module).borrow().mixin_exists(name) {
return (**module).borrow().get_mixin_no_err(name);
}
}
None
}
pub fn add_module(
&mut self,
namespace: Option<Identifier>,
module: Arc<RefCell<Module>>,
span: Span,
) -> SassResult<()> {
match namespace {
Some(namespace) => {
(*self.modules)
.borrow_mut()
.insert(namespace, module, span)?;
}
None => {
for name in (*self.scopes.global_variables()).borrow().keys() {
if (*module).borrow().var_exists(*name) {
return Err((
format!("This module and the new module both define a variable named \"{name}\".")
, span).into());
}
}
self.global_modules.push(module);
}
}
Ok(())
}
pub fn to_module(self, extension_store: ExtensionStore) -> Arc<RefCell<Module>> {
debug_assert!(self.at_root());
Arc::new(RefCell::new(Module::new_env(self, extension_store)))
}
}

9
src/evaluate/mod.rs Normal file
View File

@ -0,0 +1,9 @@
pub(crate) use bin_op::{cmp, div};
pub(crate) use env::Environment;
pub(crate) use visitor::*;
mod bin_op;
mod css_tree;
mod env;
mod scope;
mod visitor;

213
src/evaluate/scope.rs Normal file
View File

@ -0,0 +1,213 @@
use std::{
cell::{Cell, RefCell},
collections::BTreeMap,
sync::Arc,
};
use codemap::Spanned;
use crate::{
ast::Mixin,
builtin::GLOBAL_FUNCTIONS,
common::Identifier,
error::SassResult,
value::{SassFunction, Value},
};
#[allow(clippy::type_complexity)]
#[derive(Debug, Default, Clone)]
pub(crate) struct Scopes {
variables: Arc<RefCell<Vec<Arc<RefCell<BTreeMap<Identifier, Value>>>>>>,
mixins: Arc<RefCell<Vec<Arc<RefCell<BTreeMap<Identifier, Mixin>>>>>>,
functions: Arc<RefCell<Vec<Arc<RefCell<BTreeMap<Identifier, SassFunction>>>>>>,
len: Arc<Cell<usize>>,
}
impl Scopes {
pub fn new() -> Self {
Self {
variables: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])),
mixins: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])),
functions: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])),
len: Arc::new(Cell::new(1)),
}
}
pub fn new_closure(&self) -> Self {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
Self {
variables: Arc::new(RefCell::new(
(*self.variables).borrow().iter().map(Arc::clone).collect(),
)),
mixins: Arc::new(RefCell::new(
(*self.mixins).borrow().iter().map(Arc::clone).collect(),
)),
functions: Arc::new(RefCell::new(
(*self.functions).borrow().iter().map(Arc::clone).collect(),
)),
len: Arc::new(Cell::new(self.len())),
}
}
pub fn global_variables(&self) -> Arc<RefCell<BTreeMap<Identifier, Value>>> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
Arc::clone(&(*self.variables).borrow()[0])
}
pub fn global_functions(&self) -> Arc<RefCell<BTreeMap<Identifier, SassFunction>>> {
Arc::clone(&(*self.functions).borrow()[0])
}
pub fn global_mixins(&self) -> Arc<RefCell<BTreeMap<Identifier, Mixin>>> {
Arc::clone(&(*self.mixins).borrow()[0])
}
pub fn find_var(&self, name: Identifier) -> Option<usize> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for (idx, scope) in (*self.variables).borrow().iter().enumerate().rev() {
if (**scope).borrow().contains_key(&name) {
return Some(idx);
}
}
None
}
pub fn len(&self) -> usize {
(*self.len).get()
}
pub fn enter_new_scope(&mut self) {
let len = self.len();
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
(*self.len).set(len + 1);
(*self.variables)
.borrow_mut()
.push(Arc::new(RefCell::new(BTreeMap::new())));
(*self.mixins)
.borrow_mut()
.push(Arc::new(RefCell::new(BTreeMap::new())));
(*self.functions)
.borrow_mut()
.push(Arc::new(RefCell::new(BTreeMap::new())));
}
pub fn exit_scope(&mut self) {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
let len = self.len();
(*self.len).set(len - 1);
(*self.variables).borrow_mut().pop();
(*self.mixins).borrow_mut().pop();
(*self.functions).borrow_mut().pop();
}
}
/// Variables
impl Scopes {
pub fn insert_var(&mut self, idx: usize, name: Identifier, v: Value) -> Option<Value> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
(*(*self.variables).borrow_mut()[idx])
.borrow_mut()
.insert(name, v)
}
/// Always insert this variable into the innermost scope
///
/// Used, for example, for variables from `@each` and `@for`
pub fn insert_var_last(&mut self, name: Identifier, v: Value) -> Option<Value> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
(*(*self.variables).borrow_mut()[self.len() - 1])
.borrow_mut()
.insert(name, v)
}
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<Value> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for scope in (*self.variables).borrow().iter().rev() {
match (**scope).borrow().get(&name.node) {
Some(var) => return Ok(var.clone()),
None => continue,
}
}
Err(("Undefined variable.", name.span).into())
}
pub fn var_exists(&self, name: Identifier) -> bool {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for scope in (*self.variables).borrow().iter() {
if (**scope).borrow().contains_key(&name) {
return true;
}
}
false
}
}
/// Mixins
impl Scopes {
pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
(*(*self.mixins).borrow_mut().last_mut().unwrap())
.borrow_mut()
.insert(name, mixin);
}
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for scope in (*self.mixins).borrow().iter().rev() {
match (**scope).borrow().get(&name.node) {
Some(mixin) => return Ok(mixin.clone()),
None => continue,
}
}
Err(("Undefined mixin.", name.span).into())
}
pub fn mixin_exists(&self, name: Identifier) -> bool {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for scope in (*self.mixins).borrow().iter() {
if (**scope).borrow().contains_key(&name) {
return true;
}
}
false
}
}
/// Functions
impl Scopes {
pub fn insert_fn(&mut self, func: SassFunction) {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
(*(*self.functions).borrow_mut().last_mut().unwrap())
.borrow_mut()
.insert(func.name(), func);
}
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for scope in (*self.functions).borrow().iter().rev() {
let func = (**scope).borrow().get(&name).cloned();
if func.is_some() {
return func;
}
}
None
}
pub fn fn_exists(&self, name: Identifier) -> bool {
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
for scope in (*self.functions).borrow().iter() {
if (**scope).borrow().contains_key(&name) {
return true;
}
}
GLOBAL_FUNCTIONS.contains_key(name.as_str())
}
}

2878
src/evaluate/visitor.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
use std::io::{Error, ErrorKind, Result}; use std::{
use std::path::Path; io::{self, Error, ErrorKind},
path::Path,
};
/// A trait to allow replacing the file system lookup mechanisms. /// A trait to allow replacing the file system lookup mechanisms.
/// ///
@ -15,7 +17,7 @@ pub trait Fs: std::fmt::Debug {
/// Returns `true` if the path exists on disk and is pointing at a regular file. /// Returns `true` if the path exists on disk and is pointing at a regular file.
fn is_file(&self, path: &Path) -> bool; fn is_file(&self, path: &Path) -> bool;
/// Read the entire contents of a file into a bytes vector. /// Read the entire contents of a file into a bytes vector.
fn read(&self, path: &Path) -> Result<Vec<u8>>; fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
} }
/// Use [`std::fs`] to read any files from disk. /// Use [`std::fs`] to read any files from disk.
@ -36,7 +38,7 @@ impl Fs for StdFs {
} }
#[inline] #[inline]
fn read(&self, path: &Path) -> Result<Vec<u8>> { fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path) std::fs::read(path)
} }
} }
@ -61,7 +63,7 @@ impl Fs for NullFs {
} }
#[inline] #[inline]
fn read(&self, _path: &Path) -> Result<Vec<u8>> { fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
Err(Error::new( Err(Error::new(
ErrorKind::NotFound, ErrorKind::NotFound,
"NullFs, there is no file system", "NullFs, there is no file system",

View File

@ -23,6 +23,7 @@ impl InternedString {
self.resolve_ref() == "" self.resolve_ref() == ""
} }
// todo: no need for unsafe here
pub fn resolve_ref<'a>(self) -> &'a str { pub fn resolve_ref<'a>(self) -> &'a str {
unsafe { STRINGS.with(|interner| interner.as_ptr().as_ref().unwrap().resolve(&self.0)) } unsafe { STRINGS.with(|interner| interner.as_ptr().as_ref().unwrap().resolve(&self.0)) }
} }

View File

@ -1,6 +1,6 @@
use std::{borrow::Cow, iter::Peekable, str::Chars, sync::Arc}; use std::{borrow::Cow, iter::Peekable, str::Chars, sync::Arc};
use codemap::File; use codemap::{File, Span};
use crate::Token; use crate::Token;
@ -10,53 +10,70 @@ const FORM_FEED: char = '\x0C';
pub(crate) struct Lexer<'a> { pub(crate) struct Lexer<'a> {
buf: Cow<'a, [Token]>, buf: Cow<'a, [Token]>,
cursor: usize, cursor: usize,
amt_peeked: usize,
} }
impl<'a> Lexer<'a> { impl<'a> Lexer<'a> {
fn peek_cursor(&self) -> usize { pub fn raw_text(&self, start: usize) -> String {
self.cursor + self.amt_peeked self.buf[start..self.cursor]
.iter()
.map(|t| t.kind)
.collect()
}
pub fn next_char_is(&self, c: char) -> bool {
matches!(self.peek(), Some(Token { kind, .. }) if kind == c)
}
pub fn span_from(&mut self, start: usize) -> Span {
let start = match self.buf.get(start) {
Some(start) => start.pos,
None => return self.current_span(),
};
self.cursor = self.cursor.saturating_sub(1);
let end = self.current_span();
self.cursor += 1;
start.merge(end)
}
pub fn prev_span(&self) -> Span {
self.buf
.get(self.cursor.saturating_sub(1))
.copied()
.unwrap_or_else(|| self.buf.last().copied().unwrap())
.pos
}
pub fn current_span(&self) -> Span {
self.buf
.get(self.cursor)
.copied()
.unwrap_or_else(|| self.buf.last().copied().unwrap())
.pos
} }
pub fn peek(&self) -> Option<Token> { pub fn peek(&self) -> Option<Token> {
self.buf.get(self.peek_cursor()).copied() self.buf.get(self.cursor).copied()
}
pub fn reset_cursor(&mut self) {
self.amt_peeked = 0;
}
pub fn peek_next(&mut self) -> Option<Token> {
self.amt_peeked += 1;
self.peek()
} }
/// Peeks the previous token without modifying the peek cursor
pub fn peek_previous(&mut self) -> Option<Token> { pub fn peek_previous(&mut self) -> Option<Token> {
self.buf.get(self.peek_cursor().checked_sub(1)?).copied() self.buf.get(self.cursor.checked_sub(1)?).copied()
}
pub fn peek_forward(&mut self, n: usize) -> Option<Token> {
self.amt_peeked += n;
self.peek()
} }
/// Peeks `n` from current peeked position without modifying cursor /// Peeks `n` from current peeked position without modifying cursor
pub fn peek_n(&self, n: usize) -> Option<Token> { pub fn peek_n(&self, n: usize) -> Option<Token> {
self.buf.get(self.peek_cursor() + n).copied() self.buf.get(self.cursor + n).copied()
} }
pub fn peek_backward(&mut self, n: usize) -> Option<Token> { /// Peeks `n` behind current peeked position without modifying cursor
self.amt_peeked = self.amt_peeked.checked_sub(n)?; pub fn peek_n_backwards(&self, n: usize) -> Option<Token> {
self.buf.get(self.cursor.checked_sub(n)?).copied()
self.peek()
} }
/// Set cursor to position and reset peek /// Set cursor to position and reset peek
pub fn set_cursor(&mut self, cursor: usize) { pub fn set_cursor(&mut self, cursor: usize) {
self.cursor = cursor; self.cursor = cursor;
self.amt_peeked = 0;
} }
pub fn cursor(&self) -> usize { pub fn cursor(&self) -> usize {
@ -70,7 +87,6 @@ impl<'a> Iterator for Lexer<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.buf.get(self.cursor).copied().map(|tok| { self.buf.get(self.cursor).copied().map(|tok| {
self.cursor += 1; self.cursor += 1;
self.amt_peeked = self.amt_peeked.saturating_sub(1);
tok tok
}) })
} }
@ -122,15 +138,6 @@ impl<'a> Lexer<'a> {
Lexer { Lexer {
buf: Cow::Owned(buf), buf: Cow::Owned(buf),
cursor: 0, cursor: 0,
amt_peeked: 0,
}
}
pub fn new_ref(buf: &'a [Token]) -> Lexer<'a> {
Lexer {
buf: Cow::Borrowed(buf),
cursor: 0,
amt_peeked: 0,
} }
} }
} }

View File

@ -1,17 +1,14 @@
/*! # grass /*!
An implementation of Sass in pure rust. This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS.
Spec progress as of 0.11.2, released on 2022-09-03:
| Passing | Failing | Total |
|---------|---------|-------|
| 4205 | 2051 | 6256 |
## Use as library ## Use as library
``` ```
fn main() -> Result<(), Box<grass::Error>> { fn main() -> Result<(), Box<grass::Error>> {
let sass = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?; let css = grass::from_string(
assert_eq!(sass, "a b {\n color: a b;\n}\n"); "a { b { color: &; } }".to_owned(),
&grass::Options::default()
)?;
assert_eq!(css, "a b {\n color: a b;\n}\n");
Ok(()) Ok(())
} }
``` ```
@ -23,7 +20,7 @@ grass input.scss
``` ```
*/ */
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] #![warn(clippy::all, clippy::cargo)]
#![deny(missing_debug_implementations)] #![deny(missing_debug_implementations)]
#![allow( #![allow(
clippy::use_self, clippy::use_self,
@ -57,200 +54,54 @@ grass input.scss
clippy::items_after_statements, clippy::items_after_statements,
// this is only available on nightly // this is only available on nightly
clippy::unnested_or_patterns, clippy::unnested_or_patterns,
clippy::uninlined_format_args,
// todo:
clippy::cast_sign_loss,
clippy::cast_lossless,
clippy::cast_precision_loss,
clippy::float_cmp,
clippy::wildcard_imports,
clippy::comparison_chain,
clippy::bool_to_int_with_if,
)] )]
#![cfg_attr(feature = "profiling", inline(never))]
use std::path::Path; use std::path::Path;
use parse::{CssParser, SassParser, StylesheetParser};
use serializer::Serializer;
#[cfg(feature = "wasm-exports")] #[cfg(feature = "wasm-exports")]
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
pub(crate) use beef::lean::Cow;
use codemap::CodeMap; use codemap::CodeMap;
pub use crate::error::{ pub use crate::error::{
PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result, PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
}; };
pub use crate::fs::{Fs, NullFs, StdFs}; pub use crate::fs::{Fs, NullFs, StdFs};
pub(crate) use crate::token::Token; pub use crate::options::{InputSyntax, Options, OutputStyle};
use crate::{ pub(crate) use crate::{context_flags::ContextFlags, token::Token};
builtin::modules::{ModuleConfig, Modules}, use crate::{evaluate::Visitor, lexer::Lexer, parse::ScssParser};
lexer::Lexer,
output::{AtRuleContext, Css},
parse::{
common::{ContextFlags, NeverEmptyVec},
Parser,
},
scope::{Scope, Scopes},
selector::{ExtendedSelector, Extender, SelectorList},
};
mod args; mod ast;
mod atrule;
mod builtin; mod builtin;
mod color; mod color;
mod common; mod common;
mod context_flags;
mod error; mod error;
mod evaluate;
mod fs; mod fs;
mod interner; mod interner;
mod lexer; mod lexer;
mod output; mod options;
mod parse; mod parse;
mod scope;
mod selector; mod selector;
mod style; mod serializer;
mod token; mod token;
mod unit; mod unit;
mod utils; mod utils;
mod value; mod value;
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum OutputStyle {
/// The default style, this mode writes each
/// selector and declaration on its own line.
Expanded,
/// Ideal for release builds, this mode removes
/// as many extra characters as possible and
/// writes the entire stylesheet on a single line.
Compressed,
}
/// Configuration for Sass compilation
///
/// The simplest usage is `grass::Options::default()`;
/// however, a builder pattern is also exposed to offer
/// more control.
#[derive(Debug)]
pub struct Options<'a> {
fs: &'a dyn Fs,
style: OutputStyle,
load_paths: Vec<&'a Path>,
allows_charset: bool,
unicode_error_messages: bool,
quiet: bool,
}
impl Default for Options<'_> {
#[inline]
fn default() -> Self {
Self {
fs: &StdFs,
style: OutputStyle::Expanded,
load_paths: Vec::new(),
allows_charset: true,
unicode_error_messages: true,
quiet: false,
}
}
}
impl<'a> Options<'a> {
/// This option allows you to control the file system that Sass will see.
///
/// By default, it uses [`StdFs`], which is backed by [`std::fs`],
/// allowing direct, unfettered access to the local file system.
#[must_use]
#[inline]
pub fn fs(mut self, fs: &'a dyn Fs) -> Self {
self.fs = fs;
self
}
/// `grass` currently offers 2 different output styles
///
/// - `OutputStyle::Expanded` writes each selector and declaration on its own line.
/// - `OutputStyle::Compressed` removes as many extra characters as possible
/// and writes the entire stylesheet on a single line.
///
/// By default, output is expanded.
#[must_use]
#[inline]
pub const fn style(mut self, style: OutputStyle) -> Self {
self.style = style;
self
}
/// This flag tells Sass not to emit any warnings
/// when compiling. By default, Sass emits warnings
/// when deprecated features are used or when the
/// `@warn` rule is encountered. It also silences the
/// `@debug` rule.
///
/// By default, this value is `false` and warnings are emitted.
#[must_use]
#[inline]
pub const fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
/// All Sass implementations allow users to provide
/// load paths: paths on the filesystem that Sass
/// will look in when locating modules. For example,
/// if you pass `node_modules/susy/sass` as a load path,
/// you can use `@import "susy"` to load `node_modules/susy/sass/susy.scss`.
///
/// Imports will always be resolved relative to the current
/// file first, though. Load paths will only be used if no
/// relative file exists that matches the module's URL. This
/// ensures that you can't accidentally mess up your relative
/// imports when you add a new library.
///
/// This method will append a single path to the list.
#[must_use]
#[inline]
pub fn load_path(mut self, path: &'a Path) -> Self {
self.load_paths.push(path);
self
}
/// Append multiple loads paths
///
/// Note that this method does *not* remove existing load paths
///
/// See [`Options::load_path`](Options::load_path) for more information about load paths
#[must_use]
#[inline]
pub fn load_paths(mut self, paths: &'a [&'a Path]) -> Self {
self.load_paths.extend_from_slice(paths);
self
}
/// This flag tells Sass whether to emit a `@charset`
/// declaration or a UTF-8 byte-order mark.
///
/// By default, Sass will insert either a `@charset`
/// declaration (in expanded output mode) or a byte-order
/// mark (in compressed output mode) if the stylesheet
/// contains any non-ASCII characters.
#[must_use]
#[inline]
pub const fn allows_charset(mut self, allows_charset: bool) -> Self {
self.allows_charset = allows_charset;
self
}
/// This flag tells Sass only to emit ASCII characters as
/// part of error messages.
///
/// By default Sass will emit non-ASCII characters for
/// these messages.
///
/// This flag does not affect the CSS output.
#[must_use]
#[inline]
pub const fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self {
self.unicode_error_messages = unicode_error_messages;
self
}
pub(crate) fn is_compressed(&self) -> bool {
matches!(self.style, OutputStyle::Compressed)
}
}
fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> { fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
let (message, span) = err.raw(); let (message, span) = err.raw();
Box::new(Error::from_loc(message, map.look_up_span(span), unicode)) Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
@ -260,34 +111,59 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
let mut map = CodeMap::new(); let mut map = CodeMap::new();
let file = map.add_file(file_name.to_owned(), input); let file = map.add_file(file_name.to_owned(), input);
let empty_span = file.span.subspan(0, 0); let empty_span = file.span.subspan(0, 0);
let lexer = Lexer::new_from_file(&file);
let stmts = Parser { let path = Path::new(file_name);
toks: &mut Lexer::new_from_file(&file),
map: &mut map, let input_syntax = options
path: file_name.as_ref(), .input_syntax
scopes: &mut Scopes::new(), .unwrap_or_else(|| InputSyntax::for_path(path));
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(ExtendedSelector::new(SelectorList::new( let stylesheet = match input_syntax {
empty_span, InputSyntax::Scss => {
))), ScssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
span_before: empty_span,
content: &mut Vec::new(),
flags: ContextFlags::empty(),
at_root: true,
at_root_has_selector: false,
extender: &mut Extender::new(empty_span),
content_scopes: &mut Scopes::new(),
options,
modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() InputSyntax::Sass => {
SassParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
}
InputSyntax::Css => {
CssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
}
};
let stylesheet = match stylesheet {
Ok(v) => v,
Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
};
let mut visitor = Visitor::new(path, options, &mut map, empty_span);
match visitor.visit_stylesheet(stylesheet) {
Ok(_) => {}
Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
}
let stmts = visitor.finish();
let mut serializer = Serializer::new(options, &map, false, empty_span);
let mut prev_was_group_end = false;
let mut prev_requires_semicolon = false;
for stmt in stmts {
if stmt.is_invisible() {
continue;
}
let is_group_end = stmt.is_group_end();
let requires_semicolon = Serializer::requires_semicolon(&stmt);
serializer
.visit_group(stmt, prev_was_group_end, prev_requires_semicolon)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
Css::from_stmts(stmts, AtRuleContext::None, options.allows_charset) prev_was_group_end = is_group_end;
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))? prev_requires_semicolon = requires_semicolon;
.pretty_print(&map, options.style) }
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
Ok(serializer.finish(prev_requires_semicolon))
} }
/// Compile CSS from a path /// Compile CSS from a path
@ -300,8 +176,8 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)] #[inline]
pub fn from_path(p: &str, options: &Options) -> Result<String> { pub fn from_path(p: &str, options: &Options) -> Result<String> {
from_string_with_file_name( from_string_with_file_name(
String::from_utf8(options.fs.read(Path::new(p))?)?, String::from_utf8(options.fs.read(Path::new(p))?)?,
@ -319,8 +195,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)] #[inline]
pub fn from_string(input: String, options: &Options) -> Result<String> { pub fn from_string(input: String, options: &Options) -> Result<String> {
from_string_with_file_name(input, "stdin", options) from_string_with_file_name(input, "stdin", options)
} }

View File

@ -25,7 +25,6 @@ arg_enum! {
} }
} }
#[cfg_attr(feature = "profiling", inline(never))]
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
let matches = App::new("grass") let matches = App::new("grass")
.setting(AppSettings::ColoredHelp) .setting(AppSettings::ColoredHelp)
@ -144,6 +143,12 @@ fn main() -> std::io::Result<()> {
.hidden(true) .hidden(true)
.help("Whether to use terminal colors for messages.") .help("Whether to use terminal colors for messages.")
) )
.arg(
Arg::with_name("VERBOSE")
.long("verbose")
.hidden(true)
.help("Print all deprecation warnings even when they're repetitive.")
)
.arg( .arg(
Arg::with_name("NO_UNICODE") Arg::with_name("NO_UNICODE")
.long("no-unicode") .long("no-unicode")

193
src/options.rs Normal file
View File

@ -0,0 +1,193 @@
use std::path::Path;
use crate::{Fs, StdFs};
/// Configuration for Sass compilation
///
/// The simplest usage is `grass::Options::default()`;
/// however, a builder pattern is also exposed to offer
/// more control.
#[derive(Debug)]
pub struct Options<'a> {
pub(crate) fs: &'a dyn Fs,
pub(crate) style: OutputStyle,
pub(crate) load_paths: Vec<&'a Path>,
pub(crate) allows_charset: bool,
pub(crate) unicode_error_messages: bool,
pub(crate) quiet: bool,
pub(crate) input_syntax: Option<InputSyntax>,
}
impl Default for Options<'_> {
#[inline]
fn default() -> Self {
Self {
fs: &StdFs,
style: OutputStyle::Expanded,
load_paths: Vec::new(),
allows_charset: true,
unicode_error_messages: true,
quiet: false,
input_syntax: None,
}
}
}
impl<'a> Options<'a> {
/// This option allows you to control the file system that Sass will see.
///
/// By default, it uses [`StdFs`], which is backed by [`std::fs`],
/// allowing direct, unfettered access to the local file system.
#[must_use]
#[inline]
pub fn fs(mut self, fs: &'a dyn Fs) -> Self {
self.fs = fs;
self
}
/// `grass` currently offers 2 different output styles
///
/// - [`OutputStyle::Expanded`] writes each selector and declaration on its own line.
/// - [`OutputStyle::Compressed`] removes as many extra characters as possible
/// and writes the entire stylesheet on a single line.
///
/// By default, output is expanded.
#[must_use]
#[inline]
pub const fn style(mut self, style: OutputStyle) -> Self {
self.style = style;
self
}
/// This flag tells Sass not to emit any warnings
/// when compiling. By default, Sass emits warnings
/// when deprecated features are used or when the
/// `@warn` rule is encountered. It also silences the
/// `@debug` rule.
///
/// By default, this value is `false` and warnings are emitted.
#[must_use]
#[inline]
pub const fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
/// All Sass implementations allow users to provide
/// load paths: paths on the filesystem that Sass
/// will look in when locating modules. For example,
/// if you pass `node_modules/susy/sass` as a load path,
/// you can use `@import "susy"` to load `node_modules/susy/sass/susy.scss`.
///
/// Imports will always be resolved relative to the current
/// file first, though. Load paths will only be used if no
/// relative file exists that matches the module's URL. This
/// ensures that you can't accidentally mess up your relative
/// imports when you add a new library.
///
/// This method will append a single path to the list.
#[must_use]
#[inline]
pub fn load_path(mut self, path: &'a Path) -> Self {
self.load_paths.push(path);
self
}
/// Append multiple loads paths
///
/// Note that this method does *not* remove existing load paths
///
/// See [`Options::load_path`](Options::load_path) for more information about
/// load paths
#[must_use]
#[inline]
pub fn load_paths(mut self, paths: &'a [&'a Path]) -> Self {
self.load_paths.extend_from_slice(paths);
self
}
/// This flag tells Sass whether to emit a `@charset`
/// declaration or a UTF-8 byte-order mark.
///
/// By default, Sass will insert either a `@charset`
/// declaration (in expanded output mode) or a byte-order
/// mark (in compressed output mode) if the stylesheet
/// contains any non-ASCII characters.
#[must_use]
#[inline]
pub const fn allows_charset(mut self, allows_charset: bool) -> Self {
self.allows_charset = allows_charset;
self
}
/// This flag tells Sass only to emit ASCII characters as
/// part of error messages.
///
/// By default Sass will emit non-ASCII characters for
/// these messages.
///
/// This flag does not affect the CSS output.
#[must_use]
#[inline]
pub const fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self {
self.unicode_error_messages = unicode_error_messages;
self
}
/// This option forces Sass to parse input using the given syntax.
///
/// By default, Sass will attempt to read the file extension to determine
/// the syntax. If this is not possible, it will default to [`InputSyntax::Scss`]
///
/// This flag only affects the first file loaded. Files that are loaded using
/// `@import`, `@use`, or `@forward` will always have their syntax inferred.
#[must_use]
#[inline]
pub const fn input_syntax(mut self, syntax: InputSyntax) -> Self {
self.input_syntax = Some(syntax);
self
}
pub(crate) fn is_compressed(&self) -> bool {
matches!(self.style, OutputStyle::Compressed)
}
}
/// Useful when parsing Sass from sources other than the file system
///
/// See [`Options::input_syntax`] for additional information
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum InputSyntax {
/// The CSS-superset SCSS syntax.
Scss,
/// The whitespace-sensitive indented syntax.
Sass,
/// The plain CSS syntax, which disallows special Sass features.
Css,
}
impl InputSyntax {
pub(crate) fn for_path(path: &Path) -> Self {
match path.extension().and_then(|ext| ext.to_str()) {
Some("css") => Self::Css,
Some("sass") => Self::Sass,
_ => Self::Scss,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum OutputStyle {
/// This mode writes each selector and declaration on its own line.
///
/// This is the default output.
Expanded,
/// Ideal for release builds, this mode removes as many extra characters as
/// possible and writes the entire stylesheet on a single line.
Compressed,
}

View File

@ -1,782 +0,0 @@
//! # Convert from SCSS AST to CSS
use std::{io::Write, mem};
use codemap::CodeMap;
use crate::{
atrule::{
keyframes::{Keyframes, KeyframesRuleSet, KeyframesSelector},
media::MediaRule,
SupportsRule, UnknownAtRule,
},
error::SassResult,
parse::Stmt,
selector::{ComplexSelector, ComplexSelectorComponent, Selector},
style::Style,
OutputStyle,
};
#[derive(Debug, Clone)]
struct ToplevelUnknownAtRule {
name: String,
params: String,
body: Vec<Stmt>,
has_body: bool,
is_group_end: bool,
inside_rule: bool,
}
#[derive(Debug, Clone)]
struct BlockEntryUnknownAtRule {
name: String,
params: String,
}
#[derive(Debug, Clone)]
enum Toplevel {
RuleSet {
selector: Selector,
body: Vec<BlockEntry>,
is_group_end: bool,
},
MultilineComment(String),
UnknownAtRule(Box<ToplevelUnknownAtRule>),
Keyframes(Box<Keyframes>),
KeyframesRuleSet(Vec<KeyframesSelector>, Vec<BlockEntry>),
Media {
query: String,
body: Vec<Stmt>,
inside_rule: bool,
is_group_end: bool,
},
Supports {
params: String,
body: Vec<Stmt>,
inside_rule: bool,
is_group_end: bool,
},
// todo: do we actually need a toplevel style variant?
Style(Style),
Import(String),
Empty,
}
impl Toplevel {
pub fn is_invisible(&self) -> bool {
match self {
Toplevel::RuleSet { selector, body, .. } => selector.is_empty() || body.is_empty(),
Toplevel::Media { body, .. } => body.is_empty(),
Toplevel::Empty => true,
_ => false,
}
}
pub fn is_group_end(&self) -> bool {
match self {
Toplevel::RuleSet { is_group_end, .. } => *is_group_end,
Toplevel::UnknownAtRule(t) => t.is_group_end && t.inside_rule,
Toplevel::Media {
inside_rule,
is_group_end,
..
}
| Toplevel::Supports {
inside_rule,
is_group_end,
..
} => *inside_rule && *is_group_end,
_ => false,
}
}
}
fn set_group_end(group: &mut [Toplevel]) {
match group.last_mut() {
Some(Toplevel::RuleSet { is_group_end, .. })
| Some(Toplevel::Supports { is_group_end, .. })
| Some(Toplevel::Media { is_group_end, .. }) => {
*is_group_end = true;
}
Some(Toplevel::UnknownAtRule(t)) => t.is_group_end = true,
_ => {}
}
}
#[derive(Debug, Clone)]
enum BlockEntry {
Style(Style),
MultilineComment(String),
UnknownAtRule(BlockEntryUnknownAtRule),
}
impl BlockEntry {
pub fn to_string(&self) -> SassResult<String> {
match self {
BlockEntry::Style(s) => s.to_string(),
BlockEntry::MultilineComment(s) => Ok(format!("/*{}*/", s)),
BlockEntry::UnknownAtRule(BlockEntryUnknownAtRule { name, params }) => {
Ok(if params.is_empty() {
format!("@{};", name)
} else {
format!("@{} {};", name, params)
})
}
}
}
}
impl Toplevel {
const fn new_rule(selector: Selector, is_group_end: bool) -> Self {
Toplevel::RuleSet {
selector,
is_group_end,
body: Vec::new(),
}
}
fn new_keyframes_rule(selector: Vec<KeyframesSelector>) -> Self {
Toplevel::KeyframesRuleSet(selector, Vec::new())
}
fn push_style(&mut self, s: Style) {
if s.value.is_null() {
return;
}
if let Toplevel::RuleSet { body, .. } | Toplevel::KeyframesRuleSet(_, body) = self {
body.push(BlockEntry::Style(s));
} else {
panic!();
}
}
fn push_comment(&mut self, s: String) {
if let Toplevel::RuleSet { body, .. } | Toplevel::KeyframesRuleSet(_, body) = self {
body.push(BlockEntry::MultilineComment(s));
} else {
panic!();
}
}
fn push_unknown_at_rule(&mut self, at_rule: ToplevelUnknownAtRule) {
if let Toplevel::RuleSet { body, .. } = self {
body.push(BlockEntry::UnknownAtRule(BlockEntryUnknownAtRule {
name: at_rule.name,
params: at_rule.params,
}));
} else {
panic!();
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Css {
blocks: Vec<Toplevel>,
at_rule_context: AtRuleContext,
allows_charset: bool,
plain_imports: Vec<Toplevel>,
}
impl Css {
pub const fn new(at_rule_context: AtRuleContext, allows_charset: bool) -> Self {
Css {
blocks: Vec::new(),
at_rule_context,
allows_charset,
plain_imports: Vec::new(),
}
}
pub(crate) fn from_stmts(
s: Vec<Stmt>,
at_rule_context: AtRuleContext,
allows_charset: bool,
) -> SassResult<Self> {
Css::new(at_rule_context, allows_charset).parse_stylesheet(s)
}
fn parse_stmt(&mut self, stmt: Stmt) -> SassResult<Vec<Toplevel>> {
Ok(match stmt {
Stmt::RuleSet { selector, body } => {
if body.is_empty() {
return Ok(vec![Toplevel::Empty]);
}
let selector = selector.into_selector().remove_placeholders();
if selector.is_empty() {
return Ok(vec![Toplevel::Empty]);
}
let mut vals = vec![Toplevel::new_rule(selector, false)];
for rule in body {
match rule {
Stmt::RuleSet { .. } => vals.extend(self.parse_stmt(rule)?),
Stmt::Style(s) => vals.first_mut().unwrap().push_style(s),
Stmt::Comment(s) => vals.first_mut().unwrap().push_comment(s),
Stmt::Media(m) => {
let MediaRule { query, body, .. } = *m;
vals.push(Toplevel::Media {
query,
body,
inside_rule: true,
is_group_end: false,
});
}
Stmt::Supports(s) => {
let SupportsRule { params, body } = *s;
vals.push(Toplevel::Supports {
params,
body,
inside_rule: true,
is_group_end: false,
});
}
Stmt::UnknownAtRule(u) => {
let UnknownAtRule {
params,
body,
name,
has_body,
..
} = *u;
let at_rule = ToplevelUnknownAtRule {
name,
params,
body,
has_body,
inside_rule: true,
is_group_end: false,
};
if has_body {
vals.push(Toplevel::UnknownAtRule(Box::new(at_rule)));
} else {
vals.first_mut().unwrap().push_unknown_at_rule(at_rule);
}
}
Stmt::Return(..) => unreachable!(),
Stmt::AtRoot { body } => {
body.into_iter().try_for_each(|r| -> SassResult<()> {
let mut stmts = self.parse_stmt(r)?;
set_group_end(&mut stmts);
vals.append(&mut stmts);
Ok(())
})?;
}
Stmt::Keyframes(k) => {
let Keyframes { rule, name, body } = *k;
vals.push(Toplevel::Keyframes(Box::new(Keyframes {
rule,
name,
body,
})));
}
k @ Stmt::KeyframesRuleSet(..) => {
unreachable!("@keyframes ruleset {:?}", k);
}
Stmt::Import(s) => self.plain_imports.push(Toplevel::Import(s)),
};
}
vals
}
Stmt::Comment(s) => vec![Toplevel::MultilineComment(s)],
Stmt::Import(s) => {
self.plain_imports.push(Toplevel::Import(s));
Vec::new()
}
Stmt::Style(s) => vec![Toplevel::Style(s)],
Stmt::Media(m) => {
let MediaRule { query, body, .. } = *m;
vec![Toplevel::Media {
query,
body,
inside_rule: false,
is_group_end: false,
}]
}
Stmt::Supports(s) => {
let SupportsRule { params, body } = *s;
vec![Toplevel::Supports {
params,
body,
inside_rule: false,
is_group_end: false,
}]
}
Stmt::UnknownAtRule(u) => {
let UnknownAtRule {
params,
body,
name,
has_body,
..
} = *u;
vec![Toplevel::UnknownAtRule(Box::new(ToplevelUnknownAtRule {
name,
params,
body,
has_body,
inside_rule: false,
is_group_end: false,
}))]
}
Stmt::Return(..) => unreachable!("@return: {:?}", stmt),
Stmt::AtRoot { body } => body
.into_iter()
.map(|r| self.parse_stmt(r))
.collect::<SassResult<Vec<Vec<Toplevel>>>>()?
.into_iter()
.flatten()
.collect(),
Stmt::Keyframes(k) => vec![Toplevel::Keyframes(k)],
Stmt::KeyframesRuleSet(k) => {
let KeyframesRuleSet { body, selector } = *k;
if body.is_empty() {
return Ok(Vec::new());
}
let mut vals = vec![Toplevel::new_keyframes_rule(selector)];
for rule in body {
match rule {
Stmt::Style(s) => vals.first_mut().unwrap().push_style(s),
Stmt::KeyframesRuleSet(..) => vals.extend(self.parse_stmt(rule)?),
_ => todo!(),
}
}
vals
}
})
}
fn parse_stylesheet(mut self, stmts: Vec<Stmt>) -> SassResult<Css> {
for stmt in stmts {
let mut v = self.parse_stmt(stmt)?;
set_group_end(&mut v);
self.blocks.extend(v);
}
// move plain imports to top of file
self.plain_imports.append(&mut self.blocks);
mem::swap(&mut self.plain_imports, &mut self.blocks);
Ok(self)
}
pub fn pretty_print(self, map: &CodeMap, style: OutputStyle) -> SassResult<String> {
let mut buf = Vec::new();
let allows_charset = self.allows_charset;
match style {
OutputStyle::Compressed => {
CompressedFormatter::default().write_css(&mut buf, self, map)?;
}
OutputStyle::Expanded => {
ExpandedFormatter::default().write_css(&mut buf, self, map)?;
if !buf.is_empty() {
writeln!(buf)?;
}
}
}
// TODO: check for this before writing
let show_charset = allows_charset && buf.iter().any(|s| !s.is_ascii());
let out = unsafe { String::from_utf8_unchecked(buf) };
Ok(if show_charset {
match style {
OutputStyle::Compressed => format!("\u{FEFF}{}", out),
OutputStyle::Expanded => format!("@charset \"UTF-8\";\n{}", out),
}
} else {
out
})
}
}
trait Formatter {
fn write_css(&mut self, buf: &mut Vec<u8>, css: Css, map: &CodeMap) -> SassResult<()>;
}
#[derive(Debug, Default)]
struct CompressedFormatter;
impl Formatter for CompressedFormatter {
#[allow(clippy::only_used_in_recursion)]
fn write_css(&mut self, buf: &mut Vec<u8>, css: Css, map: &CodeMap) -> SassResult<()> {
for block in css.blocks {
match block {
Toplevel::RuleSet { selector, body, .. } => {
if body.is_empty() {
continue;
}
let mut complexes = selector.0.components.iter().filter(|c| !c.is_invisible());
if let Some(complex) = complexes.next() {
self.write_complex(buf, complex)?;
}
for complex in complexes {
write!(buf, ",")?;
self.write_complex(buf, complex)?;
}
write!(buf, "{{")?;
self.write_block_entry(buf, &body)?;
write!(buf, "}}")?;
}
Toplevel::KeyframesRuleSet(selectors, styles) => {
if styles.is_empty() {
continue;
}
let mut selectors = selectors.iter();
if let Some(selector) = selectors.next() {
write!(buf, "{}", selector)?;
}
for selector in selectors {
write!(buf, ",{}", selector)?;
}
write!(buf, "{{")?;
self.write_block_entry(buf, &styles)?;
write!(buf, "}}")?;
}
Toplevel::Empty | Toplevel::MultilineComment(..) => continue,
Toplevel::Import(s) => {
write!(buf, "@import {};", s)?;
}
Toplevel::UnknownAtRule(u) => {
let ToplevelUnknownAtRule {
params, name, body, ..
} = *u;
if params.is_empty() {
write!(buf, "@{}", name)?;
} else {
write!(buf, "@{} {}", name, params)?;
}
if body.is_empty() {
write!(buf, ";")?;
continue;
}
write!(buf, "{{")?;
let css = Css::from_stmts(body, AtRuleContext::Unknown, css.allows_charset)?;
self.write_css(buf, css, map)?;
write!(buf, "}}")?;
}
Toplevel::Keyframes(k) => {
let Keyframes { rule, name, body } = *k;
write!(buf, "@{}", rule)?;
if !name.is_empty() {
write!(buf, " {}", name)?;
}
if body.is_empty() {
write!(buf, "{{}}")?;
continue;
}
write!(buf, "{{")?;
let css = Css::from_stmts(body, AtRuleContext::Keyframes, css.allows_charset)?;
self.write_css(buf, css, map)?;
write!(buf, "}}")?;
}
Toplevel::Supports { params, body, .. } => {
if params.is_empty() {
write!(buf, "@supports")?;
} else {
write!(buf, "@supports {}", params)?;
}
if body.is_empty() {
write!(buf, ";")?;
continue;
}
write!(buf, "{{")?;
let css = Css::from_stmts(body, AtRuleContext::Supports, css.allows_charset)?;
self.write_css(buf, css, map)?;
write!(buf, "}}")?;
}
Toplevel::Media { query, body, .. } => {
if body.is_empty() {
continue;
}
write!(buf, "@media {}{{", query)?;
let css = Css::from_stmts(body, AtRuleContext::Media, css.allows_charset)?;
self.write_css(buf, css, map)?;
write!(buf, "}}")?;
}
Toplevel::Style(style) => {
let value = style.value.node.to_css_string(style.value.span, true)?;
write!(buf, "{}:{};", style.property, value)?;
}
}
}
Ok(())
}
}
// this could be a trait implemented on value itself
#[allow(clippy::unused_self)]
impl CompressedFormatter {
fn write_complex(&self, buf: &mut Vec<u8>, complex: &ComplexSelector) -> SassResult<()> {
let mut was_compound = false;
for component in &complex.components {
match component {
ComplexSelectorComponent::Compound(c) if was_compound => write!(buf, " {}", c)?,
ComplexSelectorComponent::Compound(c) => write!(buf, "{}", c)?,
ComplexSelectorComponent::Combinator(c) => write!(buf, "{}", c)?,
}
was_compound = matches!(component, ComplexSelectorComponent::Compound(_));
}
Ok(())
}
fn write_block_entry(&self, buf: &mut Vec<u8>, styles: &[BlockEntry]) -> SassResult<()> {
let mut styles = styles.iter();
for style in &mut styles {
match style {
BlockEntry::Style(s) => {
let value = s.value.node.to_css_string(s.value.span, true)?;
write!(buf, "{}:{}", s.property, value)?;
break;
}
BlockEntry::MultilineComment(..) => continue,
b @ BlockEntry::UnknownAtRule(_) => write!(buf, "{}", b.to_string()?)?,
}
}
for style in styles {
match style {
BlockEntry::Style(s) => {
let value = s.value.node.to_css_string(s.value.span, true)?;
write!(buf, ";{}:{}", s.property, value)?;
}
BlockEntry::MultilineComment(..) => continue,
b @ BlockEntry::UnknownAtRule(_) => write!(buf, "{}", b.to_string()?)?,
}
}
Ok(())
}
}
#[derive(Debug, Default)]
struct ExpandedFormatter {
nesting: usize,
}
#[derive(Clone, Copy)]
struct Previous {
is_group_end: bool,
}
/// What kind of @-rule are we currently inside
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AtRuleContext {
Media,
Supports,
Keyframes,
Unknown,
None,
}
impl Formatter for ExpandedFormatter {
#[allow(clippy::only_used_in_recursion)]
fn write_css(&mut self, buf: &mut Vec<u8>, css: Css, map: &CodeMap) -> SassResult<()> {
let padding = " ".repeat(self.nesting);
self.nesting += 1;
let mut prev: Option<Previous> = None;
for block in css.blocks {
if block.is_invisible() {
continue;
}
let is_group_end = block.is_group_end();
if let Some(prev) = prev {
writeln!(buf)?;
if (prev.is_group_end && css.at_rule_context == AtRuleContext::None)
|| css.at_rule_context == AtRuleContext::Supports
{
writeln!(buf)?;
}
}
match block {
Toplevel::Empty => continue,
Toplevel::RuleSet { selector, body, .. } => {
writeln!(buf, "{}{} {{", padding, selector)?;
for style in body {
writeln!(buf, "{} {}", padding, style.to_string()?)?;
}
write!(buf, "{}}}", padding)?;
}
Toplevel::KeyframesRuleSet(selector, body) => {
if body.is_empty() {
continue;
}
writeln!(
buf,
"{}{} {{",
padding,
selector
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ")
)?;
for style in body {
writeln!(buf, "{} {}", padding, style.to_string()?)?;
}
write!(buf, "{}}}", padding)?;
}
Toplevel::MultilineComment(s) => {
write!(buf, "{}/*{}*/", padding, s)?;
}
Toplevel::Import(s) => {
write!(buf, "{}@import {};", padding, s)?;
}
Toplevel::UnknownAtRule(u) => {
let ToplevelUnknownAtRule {
params,
name,
body,
has_body,
inside_rule,
..
} = *u;
if params.is_empty() {
write!(buf, "{}@{}", padding, name)?;
} else {
write!(buf, "{}@{} {}", padding, name, params)?;
}
let css = Css::from_stmts(
body,
if inside_rule {
AtRuleContext::Unknown
} else {
AtRuleContext::None
},
css.allows_charset,
)?;
if !has_body {
write!(buf, ";")?;
prev = Some(Previous { is_group_end });
continue;
}
if css.blocks.iter().all(Toplevel::is_invisible) {
write!(buf, " {{}}")?;
prev = Some(Previous { is_group_end });
continue;
}
writeln!(buf, " {{")?;
self.write_css(buf, css, map)?;
write!(buf, "\n{}}}", padding)?;
}
Toplevel::Keyframes(k) => {
let Keyframes { rule, name, body } = *k;
write!(buf, "{}@{}", padding, rule)?;
if !name.is_empty() {
write!(buf, " {}", name)?;
}
if body.is_empty() {
write!(buf, " {{}}")?;
prev = Some(Previous { is_group_end });
continue;
}
writeln!(buf, " {{")?;
let css = Css::from_stmts(body, AtRuleContext::Keyframes, css.allows_charset)?;
self.write_css(buf, css, map)?;
write!(buf, "\n{}}}", padding)?;
}
Toplevel::Supports {
params,
body,
inside_rule,
..
} => {
if params.is_empty() {
write!(buf, "{}@supports", padding)?;
} else {
write!(buf, "{}@supports {}", padding, params)?;
}
if body.is_empty() {
write!(buf, ";")?;
prev = Some(Previous { is_group_end });
continue;
}
writeln!(buf, " {{")?;
let css = Css::from_stmts(
body,
if inside_rule {
AtRuleContext::Supports
} else {
AtRuleContext::None
},
css.allows_charset,
)?;
self.write_css(buf, css, map)?;
write!(buf, "\n{}}}", padding)?;
}
Toplevel::Media {
query,
body,
inside_rule,
..
} => {
writeln!(buf, "{}@media {} {{", padding, query)?;
let css = Css::from_stmts(
body,
if inside_rule {
AtRuleContext::Media
} else {
AtRuleContext::None
},
css.allows_charset,
)?;
self.write_css(buf, css, map)?;
write!(buf, "\n{}}}", padding)?;
}
Toplevel::Style(s) => {
write!(buf, "{}{}", padding, s.to_string()?)?;
}
}
prev = Some(Previous { is_group_end });
}
self.nesting -= 1;
Ok(())
}
}

View File

@ -1,410 +0,0 @@
use std::{collections::HashMap, mem};
use codemap::Span;
use crate::{
args::{CallArg, CallArgs, FuncArg, FuncArgs},
common::QuoteKind,
error::SassResult,
scope::Scope,
utils::{read_until_closing_paren, read_until_closing_quote, read_until_newline},
value::Value,
Token,
};
use super::Parser;
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn parse_func_args(&mut self) -> SassResult<FuncArgs> {
let mut args: Vec<FuncArg> = Vec::new();
let mut close_paren_span: Span = match self.toks.peek() {
Some(Token { pos, .. }) => pos,
None => return Err(("expected \")\".", self.span_before).into()),
};
self.whitespace_or_comment();
while let Some(Token { kind, pos }) = self.toks.next() {
let name = match kind {
'$' => self.parse_identifier_no_interpolation(false)?,
')' => {
close_paren_span = pos;
break;
}
_ => return Err(("expected \")\".", pos).into()),
};
let mut default: Vec<Token> = Vec::new();
let mut is_variadic = false;
self.whitespace_or_comment();
let (kind, span) = match self.toks.next() {
Some(Token { kind, pos }) => (kind, pos),
None => return Err(("expected \")\".", pos).into()),
};
match kind {
':' => {
self.whitespace_or_comment();
while let Some(tok) = self.toks.peek() {
match &tok.kind {
',' => {
self.toks.next();
self.whitespace_or_comment();
args.push(FuncArg {
name: name.node.into(),
default: Some(default),
is_variadic,
});
break;
}
')' => {
args.push(FuncArg {
name: name.node.into(),
default: Some(default),
is_variadic,
});
close_paren_span = tok.pos();
break;
}
'(' => {
default.push(self.toks.next().unwrap());
default.extend(read_until_closing_paren(self.toks)?);
}
'/' => {
let next = self.toks.next().unwrap();
match self.toks.peek() {
Some(Token { kind: '/', .. }) => read_until_newline(self.toks),
_ => default.push(next),
};
continue;
}
&q @ '"' | &q @ '\'' => {
default.push(self.toks.next().unwrap());
default.extend(read_until_closing_quote(self.toks, q)?);
continue;
}
'\\' => {
default.push(self.toks.next().unwrap());
default.push(match self.toks.next() {
Some(tok) => tok,
None => continue,
});
}
_ => default.push(self.toks.next().unwrap()),
}
}
}
'.' => {
self.expect_char('.')?;
self.expect_char('.')?;
self.whitespace_or_comment();
self.expect_char(')')?;
is_variadic = true;
args.push(FuncArg {
name: name.node.into(),
// todo: None if empty
default: Some(default),
is_variadic,
});
break;
}
')' => {
close_paren_span = span;
args.push(FuncArg {
name: name.node.into(),
default: if default.is_empty() {
None
} else {
Some(default)
},
is_variadic,
});
break;
}
',' => args.push(FuncArg {
name: name.node.into(),
default: None,
is_variadic,
}),
_ => {}
}
self.whitespace_or_comment();
}
self.whitespace_or_comment();
// TODO: this should NOT eat the opening curly brace
// todo: self.expect_char('{')?;
match self.toks.next() {
Some(v) if v.kind == '{' => {}
Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()),
};
Ok(FuncArgs(args))
}
pub(super) fn parse_call_args(&mut self) -> SassResult<CallArgs> {
let mut args = HashMap::new();
self.whitespace_or_comment();
let mut name = String::new();
let mut span = self
.toks
.peek()
.ok_or(("expected \")\".", self.span_before))?
.pos();
loop {
self.whitespace_or_comment();
if self.consume_char_if_exists(')') {
return Ok(CallArgs(args, span));
}
if self.consume_char_if_exists(',') {
self.whitespace_or_comment();
if self.consume_char_if_exists(',') {
return Err(("expected \")\".", self.span_before).into());
}
continue;
}
if let Some(Token { kind: '$', pos }) = self.toks.peek() {
let start = self.toks.cursor();
span = span.merge(pos);
self.toks.next();
let v = self.parse_identifier_no_interpolation(false)?;
self.whitespace_or_comment();
if self.consume_char_if_exists(':') {
name = v.node;
} else {
self.toks.set_cursor(start);
name.clear();
}
} else {
name.clear();
}
self.whitespace_or_comment();
let value = self.parse_value(true, &|parser| match parser.toks.peek() {
Some(Token { kind: ')', .. }) | Some(Token { kind: ',', .. }) => true,
Some(Token { kind: '.', .. }) => {
let next_is_dot =
matches!(parser.toks.peek_n(1), Some(Token { kind: '.', .. }));
next_is_dot
}
Some(Token { kind: '=', .. }) => {
let next_is_eq = matches!(parser.toks.peek_n(1), Some(Token { kind: '=', .. }));
!next_is_eq
}
Some(..) | None => false,
});
match self.toks.peek() {
Some(Token { kind: ')', .. }) => {
self.toks.next();
args.insert(
if name.is_empty() {
CallArg::Positional(args.len())
} else {
CallArg::Named(mem::take(&mut name).into())
},
value,
);
return Ok(CallArgs(args, span));
}
Some(Token { kind: ',', .. }) => {
self.toks.next();
args.insert(
if name.is_empty() {
CallArg::Positional(args.len())
} else {
CallArg::Named(mem::take(&mut name).into())
},
value,
);
self.whitespace_or_comment();
if self.consume_char_if_exists(',') {
return Err(("expected \")\".", self.span_before).into());
}
continue;
}
Some(Token { kind: '.', pos }) => {
self.toks.next();
if let Some(Token { kind: '.', pos }) = self.toks.peek() {
if !name.is_empty() {
return Err(("expected \")\".", pos).into());
}
self.toks.next();
self.expect_char('.')?;
} else {
return Err(("expected \")\".", pos).into());
}
let val = value?;
match val.node {
Value::ArgList(v) => {
for arg in v {
args.insert(CallArg::Positional(args.len()), Ok(arg));
}
}
Value::List(v, ..) => {
for arg in v {
args.insert(
CallArg::Positional(args.len()),
Ok(arg.span(val.span)),
);
}
}
Value::Map(v) => {
// NOTE: we clone the map here because it is used
// later for error reporting. perhaps there is
// some way around this?
for (name, arg) in v.clone().entries() {
let name = match name {
Value::String(s, ..) => s,
_ => {
return Err((
format!(
"{} is not a string in {}.",
name.inspect(val.span)?,
Value::Map(v).inspect(val.span)?
),
val.span,
)
.into())
}
};
args.insert(CallArg::Named(name.into()), Ok(arg.span(val.span)));
}
}
_ => {
args.insert(CallArg::Positional(args.len()), Ok(val));
}
}
}
Some(Token { kind: '=', .. }) => {
self.toks.next();
let left = value?;
let right = self.parse_value(true, &|parser| match parser.toks.peek() {
Some(Token { kind: ')', .. }) | Some(Token { kind: ',', .. }) => true,
Some(Token { kind: '.', .. }) => {
let next_is_dot =
matches!(parser.toks.peek_n(1), Some(Token { kind: '.', .. }));
next_is_dot
}
Some(..) | None => false,
})?;
let value_span = left.span.merge(right.span);
span = span.merge(value_span);
let value = format!(
"{}={}",
left.node
.to_css_string(left.span, self.options.is_compressed())?,
right
.node
.to_css_string(right.span, self.options.is_compressed())?
);
args.insert(
if name.is_empty() {
CallArg::Positional(args.len())
} else {
CallArg::Named(mem::take(&mut name).into())
},
Ok(Value::String(value, QuoteKind::None).span(value_span)),
);
match self.toks.peek() {
Some(Token { kind: ')', .. }) => {
self.toks.next();
return Ok(CallArgs(args, span));
}
Some(Token { kind: ',', pos }) => {
span = span.merge(pos);
self.toks.next();
self.whitespace_or_comment();
continue;
}
Some(Token { kind: '.', .. }) => {
self.toks.next();
self.expect_char('.')?;
if !name.is_empty() {
return Err(("expected \")\".", self.span_before).into());
}
self.expect_char('.')?;
}
Some(Token { pos, .. }) => {
return Err(("expected \")\".", pos).into());
}
None => return Err(("expected \")\".", span).into()),
}
}
Some(Token { pos, .. }) => {
value?;
return Err(("expected \")\".", pos).into());
}
None => return Err(("expected \")\".", span).into()),
}
}
}
}
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn eval_args(
&mut self,
fn_args: &FuncArgs,
mut args: CallArgs,
) -> SassResult<Scope> {
let mut scope = Scope::new();
if fn_args.0.is_empty() {
args.max_args(0)?;
return Ok(scope);
}
if !fn_args.0.iter().any(|arg| arg.is_variadic) {
args.max_args(fn_args.len())?;
}
self.scopes.enter_new_scope();
for (idx, arg) in fn_args.0.iter().enumerate() {
if arg.is_variadic {
let arg_list = Value::ArgList(args.get_variadic()?);
scope.insert_var(arg.name, arg_list);
break;
}
let val = match args.get(idx, arg.name) {
Some(v) => v,
None => match arg.default.as_ref() {
Some(v) => self.parse_value_from_vec(v, true),
None => {
return Err(
(format!("Missing argument ${}.", &arg.name), args.span()).into()
)
}
},
}?
.node;
self.scopes.insert_var_last(arg.name, val.clone());
scope.insert_var(arg.name, val);
}
self.scopes.exit_scope();
Ok(scope)
}
}

View File

@ -0,0 +1,55 @@
use std::collections::HashSet;
use crate::{ast::AtRootQuery, error::SassResult, lexer::Lexer};
use super::BaseParser;
pub(crate) struct AtRootQueryParser<'a> {
toks: Lexer<'a>,
}
impl<'a> BaseParser<'a> for AtRootQueryParser<'a> {
fn toks(&self) -> &Lexer<'a> {
&self.toks
}
fn toks_mut(&mut self) -> &mut Lexer<'a> {
&mut self.toks
}
}
impl<'a> AtRootQueryParser<'a> {
pub fn new(toks: Lexer<'a>) -> AtRootQueryParser<'a> {
AtRootQueryParser { toks }
}
pub fn parse(&mut self) -> SassResult<AtRootQuery> {
self.expect_char('(')?;
self.whitespace()?;
let include = self.scan_identifier("with", false)?;
if !include {
self.expect_identifier("without", false)?;
}
self.whitespace()?;
self.expect_char(':')?;
self.whitespace()?;
let mut names = HashSet::new();
loop {
names.insert(self.parse_identifier(false, false)?.to_ascii_lowercase());
self.whitespace()?;
if !self.looking_at_identifier() {
break;
}
}
self.expect_char(')')?;
self.expect_done()?;
Ok(AtRootQuery::new(include, names))
}
}

695
src/parse/base.rs Normal file
View File

@ -0,0 +1,695 @@
use crate::{
error::SassResult,
lexer::Lexer,
utils::{as_hex, hex_char_for, is_name, is_name_start, opposite_bracket},
Token,
};
// todo: can we simplify lifetimes (by maybe not storing reference to lexer)
pub(crate) trait BaseParser<'a> {
fn toks(&self) -> &Lexer<'a>;
fn toks_mut(&mut self) -> &mut Lexer<'a>;
fn whitespace_without_comments(&mut self) {
while matches!(
self.toks().peek(),
Some(Token {
kind: ' ' | '\t' | '\n',
..
})
) {
self.toks_mut().next();
}
}
fn whitespace(&mut self) -> SassResult<()> {
loop {
self.whitespace_without_comments();
if !self.scan_comment()? {
break;
}
}
Ok(())
}
fn scan_comment(&mut self) -> SassResult<bool> {
if !matches!(self.toks().peek(), Some(Token { kind: '/', .. })) {
return Ok(false);
}
Ok(match self.toks().peek_n(1) {
Some(Token { kind: '/', .. }) => {
self.skip_silent_comment()?;
true
}
Some(Token { kind: '*', .. }) => {
self.skip_loud_comment()?;
true
}
_ => false,
})
}
fn skip_silent_comment(&mut self) -> SassResult<()> {
debug_assert!(self.next_matches("//"));
self.toks_mut().next();
self.toks_mut().next();
while self.toks().peek().is_some() && !self.toks().next_char_is('\n') {
self.toks_mut().next();
}
Ok(())
}
fn next_matches(&mut self, s: &str) -> bool {
for (idx, c) in s.chars().enumerate() {
match self.toks().peek_n(idx) {
Some(Token { kind, .. }) if kind == c => {}
_ => return false,
}
}
true
}
fn skip_loud_comment(&mut self) -> SassResult<()> {
debug_assert!(self.next_matches("/*"));
self.toks_mut().next();
self.toks_mut().next();
while let Some(next) = self.toks_mut().next() {
if next.kind != '*' {
continue;
}
while self.scan_char('*') {}
if self.scan_char('/') {
return Ok(());
}
}
Err(("expected more input.", self.toks().current_span()).into())
}
fn scan_char(&mut self, c: char) -> bool {
if let Some(Token { kind, .. }) = self.toks().peek() {
if kind == c {
self.toks_mut().next();
return true;
}
}
false
}
fn scan(&mut self, s: &str) -> bool {
let start = self.toks().cursor();
for c in s.chars() {
if !self.scan_char(c) {
self.toks_mut().set_cursor(start);
return false;
}
}
true
}
fn expect_whitespace(&mut self) -> SassResult<()> {
if !matches!(
self.toks().peek(),
Some(Token {
kind: ' ' | '\t' | '\n' | '\r',
..
})
) && !self.scan_comment()?
{
return Err(("Expected whitespace.", self.toks().current_span()).into());
}
self.whitespace()?;
Ok(())
}
fn parse_identifier(
&mut self,
// default=false
normalize: bool,
// default=false
unit: bool,
) -> SassResult<String> {
let mut text = String::new();
if self.scan_char('-') {
text.push('-');
if self.scan_char('-') {
text.push('-');
self.parse_identifier_body(&mut text, normalize, unit)?;
return Ok(text);
}
}
match self.toks().peek() {
Some(Token { kind: '_', .. }) if normalize => {
self.toks_mut().next();
text.push('-');
}
Some(Token { kind, .. }) if is_name_start(kind) => {
self.toks_mut().next();
text.push(kind);
}
Some(Token { kind: '\\', .. }) => {
text.push_str(&self.parse_escape(true)?);
}
Some(..) | None => {
return Err(("Expected identifier.", self.toks().current_span()).into())
}
}
self.parse_identifier_body(&mut text, normalize, unit)?;
Ok(text)
}
fn parse_identifier_body(
&mut self,
buffer: &mut String,
normalize: bool,
unit: bool,
) -> SassResult<()> {
while let Some(tok) = self.toks().peek() {
if unit && tok.kind == '-' {
// Disallow `-` followed by a dot or a digit digit in units.
let second = match self.toks().peek_n(1) {
Some(v) => v,
None => break,
};
if second.kind == '.' || second.kind.is_ascii_digit() {
break;
}
self.toks_mut().next();
buffer.push('-');
} else if normalize && tok.kind == '_' {
buffer.push('-');
self.toks_mut().next();
} else if is_name(tok.kind) {
self.toks_mut().next();
buffer.push(tok.kind);
} else if tok.kind == '\\' {
buffer.push_str(&self.parse_escape(false)?);
} else {
break;
}
}
Ok(())
}
fn parse_escape(&mut self, identifier_start: bool) -> SassResult<String> {
self.expect_char('\\')?;
let mut value = 0;
let first = match self.toks().peek() {
Some(t) => t,
None => return Err(("Expected expression.", self.toks().current_span()).into()),
};
let mut span = first.pos();
if first.kind == '\n' {
return Err(("Expected escape sequence.", span).into());
} else if first.kind.is_ascii_hexdigit() {
for _ in 0..6 {
let next = match self.toks().peek() {
Some(t) => t,
None => break,
};
if !next.kind.is_ascii_hexdigit() {
break;
}
value *= 16;
span = span.merge(next.pos);
value += as_hex(next.kind);
self.toks_mut().next();
}
if matches!(
self.toks().peek(),
Some(Token { kind: ' ', .. })
| Some(Token { kind: '\n', .. })
| Some(Token { kind: '\t', .. })
) {
self.toks_mut().next();
}
} else {
span = span.merge(first.pos);
value = first.kind as u32;
self.toks_mut().next();
}
let c = std::char::from_u32(value).ok_or(("Invalid Unicode code point.", span))?;
if (identifier_start && is_name_start(c) && !c.is_ascii_digit())
|| (!identifier_start && is_name(c))
{
Ok(c.to_string())
} else if value <= 0x1F || value == 0x7F || (identifier_start && c.is_ascii_digit()) {
let mut buf = String::with_capacity(4);
buf.push('\\');
if value > 0xF {
buf.push(hex_char_for(value >> 4));
}
buf.push(hex_char_for(value & 0xF));
buf.push(' ');
Ok(buf)
} else {
Ok(format!("\\{}", c))
}
}
fn expect_char(&mut self, c: char) -> SassResult<()> {
match self.toks().peek() {
Some(tok) if tok.kind == c => {
self.toks_mut().next();
Ok(())
}
Some(Token { pos, .. }) => Err((format!("expected \"{}\".", c), pos).into()),
None => Err((format!("expected \"{}\".", c), self.toks().current_span()).into()),
}
}
fn expect_char_with_message(&mut self, c: char, msg: &'static str) -> SassResult<()> {
match self.toks().peek() {
Some(tok) if tok.kind == c => {
self.toks_mut().next();
Ok(())
}
Some(Token { pos, .. }) => Err((format!("expected {}.", msg), pos).into()),
None => Err((format!("expected {}.", msg), self.toks().prev_span()).into()),
}
}
fn parse_string(&mut self) -> SassResult<String> {
let quote = match self.toks_mut().next() {
Some(Token {
kind: q @ ('\'' | '"'),
..
}) => q,
Some(Token { pos, .. }) => return Err(("Expected string.", pos).into()),
None => return Err(("Expected string.", self.toks().current_span()).into()),
};
let mut buffer = String::new();
let mut found_matching_quote = false;
while let Some(next) = self.toks().peek() {
if next.kind == quote {
self.toks_mut().next();
found_matching_quote = true;
break;
} else if next.kind == '\n' || next.kind == '\r' {
break;
} else if next.kind == '\\' {
if matches!(
self.toks().peek_n(1),
Some(Token {
kind: '\n' | '\r',
..
})
) {
self.toks_mut().next();
self.toks_mut().next();
} else {
buffer.push(self.consume_escaped_char()?);
}
} else {
self.toks_mut().next();
buffer.push(next.kind);
}
}
if !found_matching_quote {
return Err((format!("Expected {quote}."), self.toks().current_span()).into());
}
Ok(buffer)
}
fn consume_escaped_char(&mut self) -> SassResult<char> {
self.expect_char('\\')?;
match self.toks().peek() {
None => Ok('\u{FFFD}'),
Some(Token {
kind: '\n' | '\r',
pos,
}) => Err(("Expected escape sequence.", pos).into()),
Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => {
let mut value = 0;
for _ in 0..6 {
let next = match self.toks().peek() {
Some(c) => c,
None => break,
};
if !next.kind.is_ascii_hexdigit() {
break;
}
self.toks_mut().next();
value = (value << 4) + as_hex(next.kind);
}
if self.toks().peek().is_some()
&& self.toks().peek().unwrap().kind.is_ascii_whitespace()
{
self.toks_mut().next();
}
if value == 0 || (0xD800..=0xDFFF).contains(&value) || value >= 0x0010_FFFF {
Ok('\u{FFFD}')
} else {
Ok(char::from_u32(value).unwrap())
}
}
Some(Token { kind, .. }) => {
self.toks_mut().next();
Ok(kind)
}
}
}
fn declaration_value(&mut self, allow_empty: bool) -> SassResult<String> {
let mut buffer = String::new();
let mut brackets = Vec::new();
let mut wrote_newline = false;
while let Some(tok) = self.toks().peek() {
match tok.kind {
'\\' => {
self.toks_mut().next();
buffer.push_str(&self.parse_escape(true)?);
wrote_newline = false;
}
'"' | '\'' => {
buffer.push_str(&self.fallible_raw_text(Self::parse_string)?);
wrote_newline = false;
}
'/' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) {
buffer.push_str(&self.fallible_raw_text(Self::skip_loud_comment)?);
} else {
buffer.push('/');
self.toks_mut().next();
}
wrote_newline = false;
}
'#' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
let s = self.parse_identifier(false, false)?;
buffer.push_str(&s);
} else {
buffer.push('#');
self.toks_mut().next();
}
wrote_newline = false;
}
c @ (' ' | '\t') => {
if wrote_newline
|| !self
.toks()
.peek_n(1)
.map_or(false, |tok| tok.kind.is_ascii_whitespace())
{
buffer.push(c);
}
self.toks_mut().next();
}
'\n' | '\r' => {
if !wrote_newline {
buffer.push('\n');
}
wrote_newline = true;
self.toks_mut().next();
}
'[' | '(' | '{' => {
buffer.push(tok.kind);
self.toks_mut().next();
brackets.push(opposite_bracket(tok.kind));
wrote_newline = false;
}
']' | ')' | '}' => {
if let Some(end) = brackets.pop() {
buffer.push(tok.kind);
self.expect_char(end)?;
} else {
break;
}
wrote_newline = false;
}
';' => {
if brackets.is_empty() {
break;
}
self.toks_mut().next();
buffer.push(';');
wrote_newline = false;
}
'u' | 'U' => {
if let Some(url) = self.try_parse_url()? {
buffer.push_str(&url);
} else {
buffer.push(tok.kind);
self.toks_mut().next();
}
wrote_newline = false;
}
c => {
if self.looking_at_identifier() {
buffer.push_str(&self.parse_identifier(false, false)?);
} else {
self.toks_mut().next();
buffer.push(c);
}
wrote_newline = false;
}
}
}
if let Some(last) = brackets.pop() {
self.expect_char(last)?;
}
if !allow_empty && buffer.is_empty() {
return Err(("Expected token.", self.toks().current_span()).into());
}
Ok(buffer)
}
/// Returns whether the scanner is immediately before a plain CSS identifier.
///
/// This is based on [the CSS algorithm][], but it assumes all backslashes
/// start escapes.
///
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
fn looking_at_identifier(&self) -> bool {
match self.toks().peek() {
Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true,
Some(Token { kind: '-', .. }) => {}
Some(..) | None => return false,
}
match self.toks().peek_n(1) {
Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => true,
Some(..) | None => false,
}
}
fn try_parse_url(&mut self) -> SassResult<Option<String>> {
let start = self.toks().cursor();
if !self.scan_identifier("url", false)? {
return Ok(None);
}
if !self.scan_char('(') {
self.toks_mut().set_cursor(start);
return Ok(None);
}
self.whitespace()?;
// Match Ruby Sass's behavior: parse a raw URL() if possible, and if not
// backtrack and re-parse as a function expression.
let mut buffer = "url(".to_owned();
while let Some(next) = self.toks().peek() {
match next.kind {
'\\' => {
buffer.push_str(&self.parse_escape(false)?);
}
'!' | '#' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => {
self.toks_mut().next();
buffer.push(next.kind);
}
')' => {
self.toks_mut().next();
buffer.push(next.kind);
return Ok(Some(buffer));
}
' ' | '\t' | '\n' | '\r' => {
self.whitespace_without_comments();
if !self.toks().next_char_is(')') {
break;
}
}
_ => break,
}
}
self.toks_mut().set_cursor(start);
Ok(None)
}
fn raw_text<T>(&mut self, func: impl Fn(&mut Self) -> T) -> String {
let start = self.toks().cursor();
func(self);
self.toks().raw_text(start)
}
fn fallible_raw_text<T>(
&mut self,
func: impl Fn(&mut Self) -> SassResult<T>,
) -> SassResult<String> {
let start = self.toks().cursor();
func(self)?;
Ok(self.toks().raw_text(start))
}
/// Peeks to see if the `ident` is at the current position. If it is,
/// consume the identifier
fn scan_identifier(
&mut self,
ident: &'static str,
// default=false
case_sensitive: bool,
) -> SassResult<bool> {
if !self.looking_at_identifier() {
return Ok(false);
}
let start = self.toks().cursor();
if self.consume_identifier(ident, case_sensitive)? && !self.looking_at_identifier_body() {
Ok(true)
} else {
self.toks_mut().set_cursor(start);
Ok(false)
}
}
fn consume_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult<bool> {
for c in ident.chars() {
if !self.scan_ident_char(c, case_sensitive)? {
return Ok(false);
}
}
Ok(true)
}
fn scan_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult<bool> {
let matches = |actual: char| {
if case_sensitive {
actual == c
} else {
actual.to_ascii_lowercase() == c.to_ascii_lowercase()
}
};
Ok(match self.toks().peek() {
Some(Token { kind, .. }) if matches(kind) => {
self.toks_mut().next();
true
}
Some(Token { kind: '\\', .. }) => {
let start = self.toks().cursor();
if matches(self.consume_escaped_char()?) {
return Ok(true);
}
self.toks_mut().set_cursor(start);
false
}
Some(..) | None => false,
})
}
fn expect_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult<()> {
if self.scan_ident_char(c, case_sensitive)? {
return Ok(());
}
Err((format!("Expected \"{}\".", c), self.toks().current_span()).into())
}
fn looking_at_identifier_body(&mut self) -> bool {
matches!(self.toks().peek(), Some(t) if is_name(t.kind) || t.kind == '\\')
}
fn parse_variable_name(&mut self) -> SassResult<String> {
self.expect_char('$')?;
self.parse_identifier(true, false)
}
fn expect_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult<()> {
let start = self.toks().cursor();
for c in ident.chars() {
if !self.scan_ident_char(c, case_sensitive)? {
return Err((
format!("Expected \"{}\".", ident),
self.toks_mut().span_from(start),
)
.into());
}
}
if !self.looking_at_identifier_body() {
return Ok(());
}
Err((
format!("Expected \"{}\".", ident),
self.toks_mut().span_from(start),
)
.into())
}
// todo: not real impl
fn expect_done(&mut self) -> SassResult<()> {
debug_assert!(self.toks().peek().is_none());
Ok(())
}
fn spaces(&mut self) {
while self.toks().next_char_is(' ') || self.toks().next_char_is('\t') {
self.toks_mut().next();
}
}
}

View File

@ -1,103 +0,0 @@
use std::ops::{BitAnd, BitOr};
use codemap::Spanned;
use crate::{common::Identifier, interner::InternedString, value::Value};
#[derive(Debug, Clone)]
pub(crate) struct NeverEmptyVec<T> {
first: T,
rest: Vec<T>,
}
impl<T> NeverEmptyVec<T> {
pub const fn new(first: T) -> Self {
Self {
first,
rest: Vec::new(),
}
}
pub fn last(&self) -> &T {
self.rest.last().unwrap_or(&self.first)
}
pub fn push(&mut self, value: T) {
self.rest.push(value);
}
pub fn pop(&mut self) -> Option<T> {
self.rest.pop()
}
pub fn is_empty(&self) -> bool {
self.rest.is_empty()
}
}
/// A toplevel element beginning with something other than
/// `$`, `@`, `/`, whitespace, or a control character is either a
/// selector or a style.
#[derive(Debug)]
pub(super) enum SelectorOrStyle {
Selector(String),
Style(InternedString, Option<Box<Spanned<Value>>>),
ModuleVariableRedeclaration(Identifier),
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct ContextFlags(u8);
pub(crate) struct ContextFlag(u8);
impl ContextFlags {
pub const IN_MIXIN: ContextFlag = ContextFlag(1);
pub const IN_FUNCTION: ContextFlag = ContextFlag(1 << 1);
pub const IN_CONTROL_FLOW: ContextFlag = ContextFlag(1 << 2);
pub const IN_KEYFRAMES: ContextFlag = ContextFlag(1 << 3);
pub const IN_AT_ROOT_RULE: ContextFlag = ContextFlag(1 << 4);
pub const fn empty() -> Self {
Self(0)
}
pub fn in_mixin(self) -> bool {
(self.0 & Self::IN_MIXIN) != 0
}
pub fn in_function(self) -> bool {
(self.0 & Self::IN_FUNCTION) != 0
}
pub fn in_control_flow(self) -> bool {
(self.0 & Self::IN_CONTROL_FLOW) != 0
}
pub fn in_keyframes(self) -> bool {
(self.0 & Self::IN_KEYFRAMES) != 0
}
pub fn in_at_root_rule(self) -> bool {
(self.0 & Self::IN_AT_ROOT_RULE) != 0
}
}
impl BitAnd<ContextFlag> for u8 {
type Output = Self;
#[inline]
fn bitand(self, rhs: ContextFlag) -> Self::Output {
self & rhs.0
}
}
impl BitOr<ContextFlag> for ContextFlags {
type Output = Self;
fn bitor(self, rhs: ContextFlag) -> Self::Output {
Self(self.0 | rhs.0)
}
}
pub(crate) enum Comment {
Silent,
Loud(String),
}

View File

@ -1,396 +0,0 @@
use codemap::Spanned;
use num_traits::cast::ToPrimitive;
use crate::{
common::Identifier,
error::SassResult,
lexer::Lexer,
parse::{ContextFlags, Parser, Stmt},
unit::Unit,
utils::{read_until_closing_curly_brace, read_until_open_curly_brace},
value::{Number, Value},
Token,
};
impl<'a, 'b> Parser<'a, 'b> {
fn subparser_with_in_control_flow_flag<'c>(&'c mut self) -> Parser<'c, 'b> {
Parser {
toks: self.toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
}
fn with_toks<'d>(self, toks: &'a mut Lexer<'d>) -> Parser<'a, 'd> {
Parser {
toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags,
at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
}
pub(super) fn parse_if(&mut self) -> SassResult<Vec<Stmt>> {
self.whitespace_or_comment();
let mut found_true = false;
let mut body = Vec::new();
let init_cond = self.parse_value(true, &|_| false)?.node;
self.expect_char('{')?;
if self.toks.peek().is_none() {
return Err(("expected \"}\".", self.span_before).into());
}
if init_cond.is_true() {
found_true = true;
self.scopes.enter_new_scope();
body = self.subparser_with_in_control_flow_flag().parse_stmt()?;
self.scopes.exit_scope();
} else {
self.throw_away_until_closing_curly_brace()?;
}
loop {
self.whitespace_or_comment();
let start = self.toks.cursor();
if !self.consume_char_if_exists('@') || !self.scan_identifier("else", false) {
self.toks.set_cursor(start);
break;
}
self.whitespace_or_comment();
if let Some(tok) = self.toks.peek() {
match tok.kind {
'i' | 'I' | '\\' => {
self.span_before = tok.pos;
let mut ident = self.parse_identifier_no_interpolation(false)?;
ident.node.make_ascii_lowercase();
if ident.node != "if" {
return Err(("expected \"{\".", ident.span).into());
}
let cond = if found_true {
self.throw_away_until_open_curly_brace()?;
false
} else {
let v = self.parse_value(true, &|_| false)?.node.is_true();
self.expect_char('{')?;
v
};
if cond {
found_true = true;
self.scopes.enter_new_scope();
body = self.subparser_with_in_control_flow_flag().parse_stmt()?;
self.scopes.exit_scope();
} else {
self.throw_away_until_closing_curly_brace()?;
}
self.whitespace();
}
'{' => {
self.toks.next();
if found_true {
self.throw_away_until_closing_curly_brace()?;
break;
}
self.scopes.enter_new_scope();
let tmp = self.subparser_with_in_control_flow_flag().parse_stmt();
self.scopes.exit_scope();
return tmp;
}
_ => {
return Err(("expected \"{\".", tok.pos()).into());
}
}
} else {
break;
}
}
self.whitespace();
Ok(body)
}
pub(super) fn parse_for(&mut self) -> SassResult<Vec<Stmt>> {
self.whitespace_or_comment();
self.expect_char('$')?;
let var = self
.parse_identifier_no_interpolation(false)?
.map_node(Into::into);
self.whitespace_or_comment();
self.span_before = match self.toks.peek() {
Some(tok) => tok.pos,
None => return Err(("Expected \"from\".", var.span).into()),
};
if self.parse_identifier()?.node.to_ascii_lowercase() != "from" {
return Err(("Expected \"from\".", var.span).into());
}
self.whitespace_or_comment();
let from_val = self.parse_value(false, &|parser| match parser.toks.peek() {
Some(Token { kind: 't', .. })
| Some(Token { kind: 'T', .. })
| Some(Token { kind: '\\', .. }) => {
let start = parser.toks.cursor();
let mut ident = match parser.parse_identifier_no_interpolation(false) {
Ok(s) => s,
Err(..) => return false,
};
ident.node.make_ascii_lowercase();
let v = matches!(ident.node.to_ascii_lowercase().as_str(), "to" | "through");
parser.toks.set_cursor(start);
v
}
Some(..) | None => false,
})?;
let through = if self.scan_identifier("through", true) {
1
} else if self.scan_identifier("to", true) {
0
} else {
return Err(("Expected \"to\" or \"through\".", self.span_before).into());
};
let from = match from_val.node {
Value::Dimension(Some(n), ..) => match n.to_i32() {
Some(std::i32::MAX) | Some(std::i32::MIN) | None => {
return Err((format!("{} is not an int.", n.inspect()), from_val.span).into())
}
Some(v) => v,
},
Value::Dimension(None, ..) => return Err(("NaN is not an int.", from_val.span).into()),
v => {
return Err((
format!("{} is not a number.", v.inspect(from_val.span)?),
from_val.span,
)
.into())
}
};
let to_val = self.parse_value(true, &|_| false)?;
let to = match to_val.node {
Value::Dimension(Some(n), ..) => match n.to_i32() {
Some(std::i32::MAX) | Some(std::i32::MIN) | None => {
return Err((format!("{} is not an int.", n.inspect()), to_val.span).into())
}
Some(v) => v,
},
Value::Dimension(None, ..) => return Err(("NaN is not an int.", from_val.span).into()),
v => {
return Err((
format!(
"{} is not a number.",
v.to_css_string(to_val.span, self.options.is_compressed())?
),
to_val.span,
)
.into())
}
};
self.expect_char('{')?;
let body = read_until_closing_curly_brace(self.toks)?;
self.expect_char('}')?;
let (mut x, mut y);
// we can't use an inclusive range here
#[allow(clippy::range_plus_one)]
let iter: &mut dyn Iterator<Item = i32> = if from < to {
x = from..(to + through);
&mut x
} else {
y = ((to - through)..(from + 1)).skip(1).rev();
&mut y
};
let mut stmts = Vec::new();
self.scopes.enter_new_scope();
for i in iter {
self.scopes.insert_var_last(
var.node,
Value::Dimension(Some(Number::from(i)), Unit::None, true),
);
let mut these_stmts = self
.subparser_with_in_control_flow_flag()
.with_toks(&mut Lexer::new_ref(&body))
.parse_stmt()?;
if self.flags.in_function() {
if !these_stmts.is_empty() {
return Ok(these_stmts);
}
} else {
stmts.append(&mut these_stmts);
}
}
self.scopes.exit_scope();
Ok(stmts)
}
pub(super) fn parse_while(&mut self) -> SassResult<Vec<Stmt>> {
// technically not necessary to eat whitespace here, but since we
// operate on raw tokens rather than an AST, it potentially saves a lot of
// time in re-parsing
self.whitespace_or_comment();
let cond = read_until_open_curly_brace(self.toks)?;
if cond.is_empty() {
return Err(("Expected expression.", self.span_before).into());
}
self.toks.next();
let mut body = read_until_closing_curly_brace(self.toks)?;
body.push(match self.toks.next() {
Some(tok) => tok,
None => return Err(("expected \"}\".", self.span_before).into()),
});
let mut stmts = Vec::new();
let mut val = self.parse_value_from_vec(&cond, true)?;
self.scopes.enter_new_scope();
while val.node.is_true() {
let mut these_stmts = self
.subparser_with_in_control_flow_flag()
.with_toks(&mut Lexer::new_ref(&body))
.parse_stmt()?;
if self.flags.in_function() {
if !these_stmts.is_empty() {
return Ok(these_stmts);
}
} else {
stmts.append(&mut these_stmts);
}
val = self.parse_value_from_vec(&cond, true)?;
}
self.scopes.exit_scope();
Ok(stmts)
}
pub(super) fn parse_each(&mut self) -> SassResult<Vec<Stmt>> {
let mut vars: Vec<Spanned<Identifier>> = Vec::new();
self.whitespace_or_comment();
loop {
self.expect_char('$')?;
vars.push(self.parse_identifier()?.map_node(Into::into));
self.whitespace_or_comment();
if self
.toks
.peek()
.ok_or(("expected \"$\".", vars[vars.len() - 1].span))?
.kind
== ','
{
self.toks.next();
self.whitespace_or_comment();
} else {
break;
}
}
let i = self.parse_identifier()?;
if i.node.to_ascii_lowercase() != "in" {
return Err(("Expected \"in\".", i.span).into());
}
self.whitespace_or_comment();
let iter_val_toks = read_until_open_curly_brace(self.toks)?;
let iter = self
.parse_value_from_vec(&iter_val_toks, true)?
.node
.as_list();
self.toks.next();
self.whitespace();
let mut body = read_until_closing_curly_brace(self.toks)?;
body.push(match self.toks.next() {
Some(tok) => tok,
None => return Err(("expected \"}\".", self.span_before).into()),
});
self.whitespace();
let mut stmts = Vec::new();
self.scopes.enter_new_scope();
for row in iter {
if vars.len() == 1 {
self.scopes.insert_var_last(vars[0].node, row);
} else {
for (var, val) in vars.iter().zip(
row.as_list()
.into_iter()
.chain(std::iter::once(Value::Null).cycle()),
) {
self.scopes.insert_var_last(var.node, val);
}
}
let mut these_stmts = self
.subparser_with_in_control_flow_flag()
.with_toks(&mut Lexer::new_ref(&body))
.parse_stmt()?;
if self.flags.in_function() {
if !these_stmts.is_empty() {
return Ok(these_stmts);
}
} else {
stmts.append(&mut these_stmts);
}
}
self.scopes.exit_scope();
Ok(stmts)
}
}

219
src/parse/css.rs Normal file
View File

@ -0,0 +1,219 @@
use std::{collections::BTreeMap, path::Path};
use codemap::{CodeMap, Span, Spanned};
use crate::{
ast::*, builtin::DISALLOWED_PLAIN_CSS_FUNCTION_NAMES, common::QuoteKind, error::SassResult,
lexer::Lexer, ContextFlags, Options,
};
use super::{value::ValueParser, BaseParser, StylesheetParser};
pub(crate) struct CssParser<'a> {
pub toks: Lexer<'a>,
// todo: likely superfluous
pub map: &'a mut CodeMap,
pub path: &'a Path,
pub span_before: Span,
pub flags: ContextFlags,
pub options: &'a Options<'a>,
}
impl<'a> BaseParser<'a> for CssParser<'a> {
fn toks(&self) -> &Lexer<'a> {
&self.toks
}
fn toks_mut(&mut self) -> &mut Lexer<'a> {
&mut self.toks
}
fn skip_silent_comment(&mut self) -> SassResult<()> {
Err((
"Silent comments aren't allowed in plain CSS.",
self.toks.current_span(),
)
.into())
}
}
impl<'a> StylesheetParser<'a> for CssParser<'a> {
fn is_plain_css(&mut self) -> bool {
true
}
fn is_indented(&mut self) -> bool {
false
}
fn path(&mut self) -> &'a Path {
self.path
}
fn map(&mut self) -> &mut CodeMap {
self.map
}
fn options(&self) -> &Options {
self.options
}
fn flags(&mut self) -> &ContextFlags {
&self.flags
}
fn flags_mut(&mut self) -> &mut ContextFlags {
&mut self.flags
}
fn current_indentation(&self) -> usize {
0
}
fn span_before(&self) -> Span {
self.span_before
}
const IDENTIFIER_LIKE: Option<fn(&mut Self) -> SassResult<Spanned<AstExpr>>> =
Some(Self::parse_identifier_like);
fn parse_at_rule(
&mut self,
_child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<AstStmt> {
let start = self.toks.cursor();
self.expect_char('@')?;
let name = self.parse_interpolated_identifier()?;
self.whitespace()?;
match name.as_plain() {
Some("at-root") | Some("content") | Some("debug") | Some("each") | Some("error")
| Some("extend") | Some("for") | Some("function") | Some("if") | Some("include")
| Some("mixin") | Some("return") | Some("warn") | Some("while") => {
self.almost_any_value(false)?;
Err((
"This at-rule isn't allowed in plain CSS.",
self.toks.span_from(start),
)
.into())
}
Some("import") => self.parse_css_import_rule(start),
Some("media") => self.parse_media_rule(start),
Some("-moz-document") => self._parse_moz_document_rule(name),
Some("supports") => self.parse_supports_rule(),
_ => self.unknown_at_rule(name, start),
}
}
}
impl<'a> CssParser<'a> {
pub fn new(
toks: Lexer<'a>,
map: &'a mut CodeMap,
options: &'a Options<'a>,
span_before: Span,
file_name: &'a Path,
) -> Self {
CssParser {
toks,
map,
path: file_name,
span_before,
flags: ContextFlags::empty(),
options,
}
}
fn parse_css_import_rule(&mut self, _start: usize) -> SassResult<AstStmt> {
let url_start = self.toks.cursor();
let url = if self.toks.next_char_is('u') || self.toks.next_char_is('U') {
self.parse_dynamic_url()?
.span(self.toks.span_from(url_start))
} else {
let string = self.parse_interpolated_string()?;
AstExpr::String(
StringExpr(string.node.as_interpolation(true), QuoteKind::None),
string.span,
)
.span(string.span)
};
self.whitespace()?;
let modifiers = self.try_import_modifiers()?;
self.expect_statement_separator(Some("@import rule"))?;
Ok(AstStmt::ImportRule(AstImportRule {
imports: vec![AstImport::Plain(AstPlainCssImport {
url: Interpolation::new_with_expr(url),
modifiers,
span: self.toks.span_from(url_start),
})],
}))
}
fn parse_identifier_like(&mut self) -> SassResult<Spanned<AstExpr>> {
let start = self.toks.cursor();
let identifier = self.parse_interpolated_identifier()?;
let plain = identifier.as_plain().unwrap();
let lower = plain.to_ascii_lowercase();
if let Some(special_fn) = ValueParser::try_parse_special_function(self, &lower, start)? {
return Ok(special_fn);
}
let before_args = self.toks.cursor();
if !self.scan_char('(') {
let span = self.toks.span_from(start);
return Ok(AstExpr::String(StringExpr(identifier, QuoteKind::None), span).span(span));
}
let allow_empty_second_arg = lower == "var";
let mut arguments = Vec::new();
if !self.scan_char(')') {
loop {
self.whitespace()?;
let arg_start = self.toks.cursor();
if allow_empty_second_arg && arguments.len() == 1 && self.toks.next_char_is(')') {
arguments.push(AstExpr::String(
StringExpr(Interpolation::new_plain(String::new()), QuoteKind::None),
self.toks.span_from(arg_start),
));
break;
}
arguments.push(self.parse_expression_until_comma(true)?.node);
self.whitespace()?;
if !self.scan_char(',') {
break;
}
}
self.expect_char(')')?;
}
let span = self.toks.span_from(start);
if DISALLOWED_PLAIN_CSS_FUNCTION_NAMES.contains(plain) {
return Err(("This function isn't allowed in plain CSS.", span).into());
}
Ok(AstExpr::InterpolatedFunction(InterpolatedFunction {
name: identifier,
arguments: Box::new(ArgumentInvocation {
positional: arguments,
named: BTreeMap::new(),
rest: None,
keyword_rest: None,
span: self.toks.span_from(before_args),
}),
span,
})
.span(span))
}
}

View File

@ -1,166 +0,0 @@
use codemap::Spanned;
use crate::{
args::CallArgs,
atrule::Function,
common::{unvendor, Identifier},
error::SassResult,
lexer::Lexer,
scope::Scopes,
utils::read_until_closing_curly_brace,
value::{SassFunction, Value},
};
use super::{common::ContextFlags, Parser, Stmt};
/// Names that functions are not allowed to have
const RESERVED_IDENTIFIERS: [&str; 8] = [
"calc",
"element",
"expression",
"url",
"and",
"or",
"not",
"clamp",
];
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn parse_function(&mut self) -> SassResult<()> {
self.whitespace_or_comment();
let Spanned { node: name, span } = self.parse_identifier()?;
if self.flags.in_mixin() {
return Err(("Mixins may not contain function declarations.", span).into());
}
if self.flags.in_control_flow() {
return Err(("Functions may not be declared in control directives.", span).into());
}
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) {
return Err(("Invalid function name.", span).into());
}
self.whitespace_or_comment();
self.expect_char('(')?;
let args = self.parse_func_args()?;
self.whitespace();
let mut body = read_until_closing_curly_brace(self.toks)?;
body.push(match self.toks.next() {
Some(tok) => tok,
None => return Err(("expected \"}\".", self.span_before).into()),
});
self.whitespace();
let function = Function::new(args, body, self.at_root, span);
let name_as_ident = Identifier::from(name);
let sass_function = SassFunction::UserDefined {
function: Box::new(function),
name: name_as_ident,
};
if self.at_root {
self.global_scope.insert_fn(name_as_ident, sass_function);
} else {
self.scopes.insert_fn(name_as_ident, sass_function);
}
Ok(())
}
pub(super) fn parse_return(&mut self) -> SassResult<Box<Value>> {
let v = self.parse_value(true, &|_| false)?;
self.consume_char_if_exists(';');
Ok(Box::new(v.node))
}
pub fn eval_function(
&mut self,
function: Function,
args: CallArgs,
module: Option<Spanned<Identifier>>,
) -> SassResult<Value> {
let Function {
body,
args: fn_args,
declared_at_root,
..
} = function;
let scope = self.eval_args(&fn_args, args)?;
let mut new_scope = Scopes::new();
let mut entered_scope = false;
if declared_at_root {
new_scope.enter_scope(scope);
} else {
entered_scope = true;
self.scopes.enter_scope(scope);
};
if let Some(module) = module {
let module = self.modules.get(module.node, module.span)?;
if declared_at_root {
new_scope.enter_scope(module.scope.clone());
} else {
self.scopes.enter_scope(module.scope.clone());
}
}
let mut return_value = Parser {
toks: &mut Lexer::new(body),
map: self.map,
path: self.path,
scopes: if declared_at_root {
&mut new_scope
} else {
self.scopes
},
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_FUNCTION,
at_root: false,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse_stmt()?;
if entered_scope {
self.scopes.exit_scope();
}
if module.is_some() {
self.scopes.exit_scope();
}
debug_assert!(
return_value.len() <= 1,
"we expect there to be only one return value"
);
match return_value
.pop()
.ok_or(("Function finished without @return.", self.span_before))?
{
Stmt::Return(v) => Ok(*v),
_ => todo!("should be unreachable"),
}
}
}

View File

@ -1,366 +0,0 @@
use std::{borrow::Borrow, iter::Iterator};
use codemap::Spanned;
use crate::{
common::QuoteKind,
error::SassResult,
utils::{as_hex, hex_char_for, is_name, is_name_start},
value::Value,
Token,
};
use super::Parser;
impl<'a, 'b> Parser<'a, 'b> {
fn ident_body_no_interpolation(&mut self, unit: bool) -> SassResult<Spanned<String>> {
let mut text = String::new();
while let Some(tok) = self.toks.peek() {
self.span_before = self.span_before.merge(tok.pos());
if unit && tok.kind == '-' {
// Disallow `-` followed by a dot or a digit digit in units.
let second = match self.toks.peek_forward(1) {
Some(v) => v,
None => break,
};
self.toks.peek_backward(1).unwrap();
if second.kind == '.' || second.kind.is_ascii_digit() {
break;
}
self.toks.next();
text.push('-');
} else if is_name(tok.kind) {
text.push(self.toks.next().unwrap().kind);
} else if tok.kind == '\\' {
self.toks.next();
text.push_str(&self.parse_escape(false)?);
} else {
break;
}
}
Ok(Spanned {
node: text,
span: self.span_before,
})
}
fn interpolated_ident_body(&mut self, buf: &mut String) -> SassResult<()> {
while let Some(tok) = self.toks.peek() {
match tok.kind {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => {
self.span_before = self.span_before.merge(tok.pos());
buf.push(self.toks.next().unwrap().kind);
}
'\\' => {
self.toks.next();
buf.push_str(&self.parse_escape(false)?);
}
'#' => {
if let Some(Token { kind: '{', .. }) = self.toks.peek_forward(1) {
self.toks.next();
self.toks.next();
// TODO: if ident, interpolate literally
let interpolation = self.parse_interpolation()?;
buf.push_str(
&interpolation
.node
.to_css_string(interpolation.span, self.options.is_compressed())?,
);
} else {
self.toks.reset_cursor();
break;
}
}
_ => break,
}
}
Ok(())
}
pub(crate) fn parse_escape(&mut self, identifier_start: bool) -> SassResult<String> {
let mut value = 0;
let first = match self.toks.peek() {
Some(t) => t,
None => return Err(("Expected expression.", self.span_before).into()),
};
let mut span = first.pos();
if first.kind == '\n' {
return Err(("Expected escape sequence.", span).into());
} else if first.kind.is_ascii_hexdigit() {
for _ in 0..6 {
let next = match self.toks.peek() {
Some(t) => t,
None => break,
};
if !next.kind.is_ascii_hexdigit() {
break;
}
value *= 16;
span = span.merge(next.pos);
value += as_hex(next.kind);
self.toks.next();
}
if matches!(
self.toks.peek(),
Some(Token { kind: ' ', .. })
| Some(Token { kind: '\n', .. })
| Some(Token { kind: '\t', .. })
) {
self.toks.next();
}
} else {
span = span.merge(first.pos);
value = first.kind as u32;
self.toks.next();
}
let c = std::char::from_u32(value).ok_or(("Invalid Unicode code point.", span))?;
if (identifier_start && is_name_start(c) && !c.is_ascii_digit())
|| (!identifier_start && is_name(c))
{
Ok(c.to_string())
} else if value <= 0x1F || value == 0x7F || (identifier_start && c.is_ascii_digit()) {
let mut buf = String::with_capacity(4);
buf.push('\\');
if value > 0xF {
buf.push(hex_char_for(value >> 4));
}
buf.push(hex_char_for(value & 0xF));
buf.push(' ');
Ok(buf)
} else {
Ok(format!("\\{}", c))
}
}
pub(crate) fn parse_identifier(&mut self) -> SassResult<Spanned<String>> {
let Token { kind, pos } = self
.toks
.peek()
.ok_or(("Expected identifier.", self.span_before))?;
let mut text = String::new();
if kind == '-' {
self.toks.next();
text.push('-');
match self.toks.peek() {
Some(Token { kind: '-', .. }) => {
self.toks.next();
text.push('-');
self.interpolated_ident_body(&mut text)?;
return Ok(Spanned {
node: text,
span: pos,
});
}
Some(..) => {}
None => {
return Ok(Spanned {
node: text,
span: self.span_before,
})
}
}
}
let Token { kind: first, pos } = match self.toks.peek() {
Some(v) => v,
None => return Err(("Expected identifier.", self.span_before).into()),
};
match first {
c if is_name_start(c) => {
text.push(self.toks.next().unwrap().kind);
}
'\\' => {
self.toks.next();
text.push_str(&self.parse_escape(true)?);
}
'#' if matches!(self.toks.peek_forward(1), Some(Token { kind: '{', .. })) => {
self.toks.next();
self.toks.next();
match self.parse_interpolation()?.node {
Value::String(ref s, ..) => text.push_str(s),
v => text.push_str(
v.to_css_string(self.span_before, self.options.is_compressed())?
.borrow(),
),
}
}
_ => return Err(("Expected identifier.", pos).into()),
}
self.interpolated_ident_body(&mut text)?;
Ok(Spanned {
node: text,
span: self.span_before,
})
}
pub(crate) fn parse_identifier_no_interpolation(
&mut self,
unit: bool,
) -> SassResult<Spanned<String>> {
let Token {
kind,
pos: mut span,
} = self
.toks
.peek()
.ok_or(("Expected identifier.", self.span_before))?;
let mut text = String::new();
if kind == '-' {
self.toks.next();
text.push('-');
match self.toks.peek() {
Some(Token { kind: '-', .. }) => {
self.toks.next();
text.push('-');
text.push_str(&self.ident_body_no_interpolation(unit)?.node);
return Ok(Spanned { node: text, span });
}
Some(..) => {}
None => return Ok(Spanned { node: text, span }),
}
}
let first = match self.toks.next() {
Some(v) => v,
None => return Err(("Expected identifier.", span).into()),
};
if is_name_start(first.kind) {
text.push(first.kind);
} else if first.kind == '\\' {
text.push_str(&self.parse_escape(true)?);
} else {
return Err(("Expected identifier.", first.pos).into());
}
let body = self.ident_body_no_interpolation(unit)?;
span = span.merge(body.span);
text.push_str(&body.node);
Ok(Spanned { node: text, span })
}
pub(crate) fn parse_quoted_string(&mut self, q: char) -> SassResult<Spanned<Value>> {
let mut s = String::new();
let mut span = self
.toks
.peek()
.ok_or((format!("Expected {}.", q), self.span_before))?
.pos();
while let Some(tok) = self.toks.next() {
span = span.merge(tok.pos());
match tok.kind {
'"' if q == '"' => {
return Ok(Spanned {
node: Value::String(s, QuoteKind::Quoted),
span,
});
}
'\'' if q == '\'' => {
return Ok(Spanned {
node: Value::String(s, QuoteKind::Quoted),
span,
})
}
'#' => {
if let Some(Token { kind: '{', pos }) = self.toks.peek() {
self.span_before = self.span_before.merge(pos);
self.toks.next();
let interpolation = self.parse_interpolation()?;
match interpolation.node {
Value::String(ref v, ..) => s.push_str(v),
v => s.push_str(
v.to_css_string(interpolation.span, self.options.is_compressed())?
.borrow(),
),
};
continue;
}
s.push('#');
continue;
}
'\n' => return Err(("Expected \".", tok.pos()).into()),
'\\' => {
let first = match self.toks.peek() {
Some(c) => c,
None => {
s.push('\u{FFFD}');
continue;
}
};
if first.kind == '\n' {
self.toks.next();
continue;
}
if first.kind.is_ascii_hexdigit() {
let mut value = 0;
for _ in 0..6 {
let next = match self.toks.peek() {
Some(c) => c,
None => break,
};
if !next.kind.is_ascii_hexdigit() {
break;
}
value = (value << 4) + as_hex(self.toks.next().unwrap().kind);
}
if self.toks.peek().is_some()
&& self.toks.peek().unwrap().kind.is_ascii_whitespace()
{
self.toks.next();
}
if value == 0 || (0xD800..=0xDFFF).contains(&value) || value >= 0x0010_FFFF
{
s.push('\u{FFFD}');
} else {
s.push(std::char::from_u32(value).unwrap());
}
} else {
s.push(self.toks.next().unwrap().kind);
}
}
_ => s.push(tok.kind),
}
}
Err((format!("Expected {}.", q), span).into())
}
/// Returns whether the scanner is immediately before a plain CSS identifier.
///
// todo: foward arg
/// If `forward` is passed, this looks that many characters forward instead.
///
/// This is based on [the CSS algorithm][], but it assumes all backslashes
/// start escapes.
///
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
pub fn looking_at_identifier(&mut self) -> bool {
match self.toks.peek() {
Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true,
Some(Token { kind: '-', .. }) => {}
Some(..) | None => return false,
}
match self.toks.peek_forward(1) {
Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => {
self.toks.reset_cursor();
true
}
Some(..) | None => {
self.toks.reset_cursor();
false
}
}
}
}

View File

@ -1,189 +0,0 @@
use std::{ffi::OsStr, path::Path, path::PathBuf};
use codemap::{Span, Spanned};
use crate::{
common::{ListSeparator::Comma, QuoteKind},
error::SassResult,
lexer::Lexer,
value::Value,
Token,
};
use super::{Parser, Stmt};
#[allow(clippy::case_sensitive_file_extension_comparisons)]
fn is_plain_css_import(url: &str) -> bool {
if url.len() < 5 {
return false;
}
let lower = url.to_ascii_lowercase();
lower.ends_with(".css")
|| lower.starts_with("http://")
|| lower.starts_with("https://")
|| lower.starts_with("//")
}
impl<'a, 'b> Parser<'a, 'b> {
/// Searches the current directory of the file then searches in `load_paths` directories
/// if the import has not yet been found.
///
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
pub(super) fn find_import(&self, path: &Path) -> Option<PathBuf> {
let path_buf = if path.is_absolute() {
// todo: test for absolute path imports
path.into()
} else {
self.path
.parent()
.unwrap_or_else(|| Path::new(""))
.join(path)
};
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new(".."));
macro_rules! try_path {
($name:expr) => {
let name = $name;
if self.options.fs.is_file(&name) {
return Some(name);
}
};
}
try_path!(path_buf.with_file_name(name).with_extension("scss"));
try_path!(path_buf
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"));
try_path!(path_buf.clone());
try_path!(path_buf.join("index.scss"));
try_path!(path_buf.join("_index.scss"));
for path in &self.options.load_paths {
if self.options.fs.is_dir(path) {
try_path!(path.join(name).with_extension("scss"));
try_path!(path
.join(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"));
try_path!(path.join("index.scss"));
try_path!(path.join("_index.scss"));
} else {
try_path!(path.to_path_buf());
try_path!(path.with_file_name(name).with_extension("scss"));
try_path!(path
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"));
try_path!(path.join("index.scss"));
try_path!(path.join("_index.scss"));
}
}
None
}
pub(crate) fn parse_single_import(
&mut self,
file_name: &str,
span: Span,
) -> SassResult<Vec<Stmt>> {
let path: &Path = file_name.as_ref();
if let Some(name) = self.find_import(path) {
let file = self.map.add_file(
name.to_string_lossy().into(),
String::from_utf8(self.options.fs.read(&name)?)?,
);
return Parser {
toks: &mut Lexer::new_from_file(&file),
map: self.map,
path: &name,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: file.span.subspan(0, 0),
content: self.content,
flags: self.flags,
at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse();
}
Err(("Can't find stylesheet to import.", span).into())
}
pub(super) fn import(&mut self) -> SassResult<Vec<Stmt>> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
self.whitespace_or_comment();
match self.toks.peek() {
Some(Token { kind: '\'', .. })
| Some(Token { kind: '"', .. })
| Some(Token { kind: 'u', .. }) => {}
Some(Token { pos, .. }) => return Err(("Expected string.", pos).into()),
None => return Err(("expected more input.", self.span_before).into()),
};
let Spanned {
node: file_name_as_value,
span,
} = self.parse_value(true, &|_| false)?;
match file_name_as_value {
Value::String(s, QuoteKind::Quoted) => {
if is_plain_css_import(&s) {
Ok(vec![Stmt::Import(format!("\"{}\"", s))])
} else {
self.parse_single_import(&s, span)
}
}
Value::String(s, QuoteKind::None) => {
if s.starts_with("url(") {
Ok(vec![Stmt::Import(s)])
} else {
self.parse_single_import(&s, span)
}
}
Value::List(v, Comma, _) => {
let mut list_of_imports: Vec<Stmt> = Vec::new();
for file_name_element in v {
match file_name_element {
#[allow(clippy::case_sensitive_file_extension_comparisons)]
Value::String(s, QuoteKind::Quoted) => {
let lower = s.to_ascii_lowercase();
if lower.ends_with(".css")
|| lower.starts_with("http://")
|| lower.starts_with("https://")
{
list_of_imports.push(Stmt::Import(format!("\"{}\"", s)));
} else {
list_of_imports.append(&mut self.parse_single_import(&s, span)?);
}
}
Value::String(s, QuoteKind::None) => {
if s.starts_with("url(") {
list_of_imports.push(Stmt::Import(s));
} else {
list_of_imports.append(&mut self.parse_single_import(&s, span)?);
}
}
_ => return Err(("Expected string.", span).into()),
}
}
Ok(list_of_imports)
}
_ => Err(("Expected string.", span).into()),
}
}
}

View File

@ -1,14 +1,8 @@
use std::fmt; use std::fmt;
use crate::{ use crate::{ast::KeyframesSelector, error::SassResult, lexer::Lexer, token::Token};
atrule::keyframes::{Keyframes, KeyframesSelector},
error::SassResult,
lexer::Lexer,
parse::Stmt,
Token,
};
use super::{common::ContextFlags, Parser}; use super::BaseParser;
impl fmt::Display for KeyframesSelector { impl fmt::Display for KeyframesSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -20,201 +14,128 @@ impl fmt::Display for KeyframesSelector {
} }
} }
struct KeyframesSelectorParser<'a, 'b, 'c> { pub(crate) struct KeyframesSelectorParser<'a> {
parser: &'a mut Parser<'b, 'c>, toks: Lexer<'a>,
} }
impl<'a, 'b, 'c> KeyframesSelectorParser<'a, 'b, 'c> { impl<'a> BaseParser<'a> for KeyframesSelectorParser<'a> {
pub fn new(parser: &'a mut Parser<'b, 'c>) -> Self { fn toks(&self) -> &Lexer<'a> {
Self { parser } &self.toks
} }
fn parse_keyframes_selector(&mut self) -> SassResult<Vec<KeyframesSelector>> { fn toks_mut(&mut self) -> &mut Lexer<'a> {
&mut self.toks
}
}
impl<'a> KeyframesSelectorParser<'a> {
pub fn new(toks: Lexer<'a>) -> KeyframesSelectorParser<'a> {
KeyframesSelectorParser { toks }
}
pub fn parse_keyframes_selector(&mut self) -> SassResult<Vec<KeyframesSelector>> {
let mut selectors = Vec::new(); let mut selectors = Vec::new();
self.parser.whitespace_or_comment(); loop {
while let Some(tok) = self.parser.toks.peek() { self.whitespace()?;
match tok.kind { if self.looking_at_identifier() {
't' | 'T' => { if self.scan_identifier("to", false)? {
let mut ident = self.parser.parse_identifier()?;
ident.node.make_ascii_lowercase();
if ident.node == "to" {
selectors.push(KeyframesSelector::To); selectors.push(KeyframesSelector::To);
} else { } else if self.scan_identifier("from", false)? {
return Err(("Expected \"to\" or \"from\".", tok.pos).into());
}
}
'f' | 'F' => {
let mut ident = self.parser.parse_identifier()?;
ident.node.make_ascii_lowercase();
if ident.node == "from" {
selectors.push(KeyframesSelector::From); selectors.push(KeyframesSelector::From);
} else { } else {
return Err(("Expected \"to\" or \"from\".", tok.pos).into()); return Err(("Expected \"to\" or \"from\".", self.toks.current_span()).into());
} }
}
'0'..='9' => {
let mut num = self.parser.parse_whole_number();
if let Some(Token { kind: '.', .. }) = self.parser.toks.peek() {
self.parser.toks.next();
num.push('.');
num.push_str(&self.parser.parse_whole_number());
}
self.parser.expect_char('%')?;
selectors.push(KeyframesSelector::Percent(num.into_boxed_str()));
}
'{' => break,
'\\' => todo!("escaped chars in @keyframes selector"),
_ => return Err(("Expected \"to\" or \"from\".", tok.pos).into()),
}
self.parser.whitespace_or_comment();
if let Some(Token { kind: ',', .. }) = self.parser.toks.peek() {
self.parser.toks.next();
self.parser.whitespace_or_comment();
} else { } else {
selectors.push(self.parse_percentage_selector()?);
}
self.whitespace()?;
if !self.scan_char(',') {
break; break;
} }
} }
Ok(selectors) Ok(selectors)
} }
fn parse_percentage_selector(&mut self) -> SassResult<KeyframesSelector> {
let mut buffer = String::new();
if self.scan_char('+') {
buffer.push('+');
} }
impl<'a, 'b> Parser<'a, 'b> { if !matches!(
fn parse_keyframes_name(&mut self) -> SassResult<String> { self.toks.peek(),
let mut name = String::new(); Some(Token {
self.whitespace_or_comment(); kind: '0'..='9' | '.',
while let Some(tok) = self.toks.next() { ..
match tok.kind {
'#' => {
if self.consume_char_if_exists('{') {
name.push_str(&self.parse_interpolation_as_string()?);
} else {
name.push('#');
}
}
' ' | '\n' | '\t' => {
self.whitespace();
name.push(' ');
}
'{' => {
// todo: we can avoid the reallocation by trimming before emitting
// (in `output.rs`)
return Ok(name.trim().to_owned());
}
_ => name.push(tok.kind),
}
}
Err(("expected \"{\".", self.span_before).into())
}
pub(super) fn parse_keyframes_selector(
&mut self,
mut string: String,
) -> SassResult<Vec<KeyframesSelector>> {
let mut span = if let Some(tok) = self.toks.peek() {
tok.pos()
} else {
return Err(("expected \"{\".", self.span_before).into());
};
self.span_before = span;
while let Some(tok) = self.toks.next() {
span = span.merge(tok.pos());
match tok.kind {
'#' => {
if self.consume_char_if_exists('{') {
string.push_str(
&self
.parse_interpolation()?
.to_css_string(span, self.options.is_compressed())?,
);
} else {
string.push('#');
}
}
',' => {
while let Some(c) = string.pop() {
if c == ' ' || c == ',' {
continue;
}
string.push(c);
string.push(',');
break;
}
}
'/' => {
if self.toks.peek().is_none() {
return Err(("Expected selector.", tok.pos()).into());
}
self.parse_comment()?;
self.whitespace();
string.push(' ');
}
'{' => {
let sel_toks: Vec<Token> =
string.chars().map(|x| Token::new(span, x)).collect();
let selector = KeyframesSelectorParser::new(&mut Parser {
toks: &mut Lexer::new(sel_toks),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags,
at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}) })
.parse_keyframes_selector()?; ) {
return Err(("Expected number.", self.toks.current_span()).into());
return Ok(selector);
} }
c => string.push(c),
while matches!(
self.toks.peek(),
Some(Token {
kind: '0'..='9',
..
})
) {
buffer.push(self.toks.next().unwrap().kind);
}
if self.scan_char('.') {
buffer.push('.');
while matches!(
self.toks.peek(),
Some(Token {
kind: '0'..='9',
..
})
) {
buffer.push(self.toks.next().unwrap().kind);
} }
} }
Err(("expected \"{\".", span).into()) if self.scan_ident_char('e', false)? {
buffer.push('e');
if matches!(
self.toks.peek(),
Some(Token {
kind: '+' | '-',
..
})
) {
buffer.push(self.toks.next().unwrap().kind);
} }
pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult<Stmt> { if !matches!(
if self.flags.in_function() { self.toks.peek(),
return Err(("This at-rule is not allowed here.", self.span_before).into()); Some(Token {
kind: '0'..='9',
..
})
) {
return Err(("Expected digit.", self.toks.current_span()).into());
} }
let name = self.parse_keyframes_name()?; while matches!(
self.toks.peek(),
self.whitespace(); Some(Token {
kind: '0'..='9',
let body = Parser { ..
toks: self.toks, })
map: self.map, ) {
path: self.path, buffer.push(self.toks.next().unwrap().kind);
scopes: self.scopes, }
global_scope: self.global_scope, }
super_selectors: self.super_selectors,
span_before: self.span_before, self.expect_char('%')?;
content: self.content,
flags: self.flags | ContextFlags::IN_KEYFRAMES, Ok(KeyframesSelector::Percent(buffer.into_boxed_str()))
at_root: false,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse_stmt()?;
Ok(Stmt::Keyframes(Box::new(Keyframes { rule, name, body })))
} }
} }

View File

@ -1,175 +0,0 @@
use crate::{
error::SassResult,
utils::is_name_start,
{Cow, Token},
};
use super::Parser;
impl<'a, 'b> Parser<'a, 'b> {
/// Peeks to see if the `ident` is at the current position. If it is,
/// consume the identifier
pub fn scan_identifier(&mut self, ident: &'static str, case_insensitive: bool) -> bool {
let start = self.toks.cursor();
let mut peeked_identifier = match self.parse_identifier_no_interpolation(false) {
Ok(v) => v.node,
Err(..) => {
self.toks.set_cursor(start);
return false;
}
};
if case_insensitive {
peeked_identifier.make_ascii_lowercase();
}
if peeked_identifier == ident {
return true;
}
self.toks.set_cursor(start);
false
}
pub fn expression_until_comparison(&mut self) -> SassResult<Cow<'static, str>> {
let value = self.parse_value(false, &|parser| match parser.toks.peek() {
Some(Token { kind: '>', .. })
| Some(Token { kind: '<', .. })
| Some(Token { kind: ':', .. })
| Some(Token { kind: ')', .. }) => true,
Some(Token { kind: '=', .. }) => {
let is_double_eq = matches!(parser.toks.peek_next(), Some(Token { kind: '=', .. }));
parser.toks.reset_cursor();
// if it is a double eq, then parse as normal
//
// otherwise, it is a single eq and we should
// treat it as a comparison
!is_double_eq
}
_ => false,
})?;
value
.node
.unquote()
.to_css_string(value.span, self.options.is_compressed())
}
pub(super) fn parse_media_query_list(&mut self) -> SassResult<String> {
let mut buf = String::new();
loop {
self.whitespace_or_comment();
buf.push_str(&self.parse_single_media_query()?);
if !self.consume_char_if_exists(',') {
break;
}
buf.push(',');
buf.push(' ');
}
Ok(buf)
}
fn parse_media_feature(&mut self) -> SassResult<String> {
if self.consume_char_if_exists('#') {
self.expect_char('{')?;
return Ok(self.parse_interpolation_as_string()?.into_owned());
}
let mut buf = String::with_capacity(2);
self.expect_char('(')?;
buf.push('(');
self.whitespace_or_comment();
buf.push_str(&self.expression_until_comparison()?);
if self.consume_char_if_exists(':') {
self.whitespace_or_comment();
buf.push(':');
buf.push(' ');
let value = self.parse_value(false, &|parser| {
matches!(parser.toks.peek(), Some(Token { kind: ')', .. }))
})?;
self.expect_char(')')?;
buf.push_str(
&value
.node
.to_css_string(value.span, self.options.is_compressed())?,
);
self.whitespace_or_comment();
buf.push(')');
return Ok(buf);
}
let next_tok = self.toks.peek();
let is_angle = next_tok.map_or(false, |t| t.kind == '<' || t.kind == '>');
if is_angle || matches!(next_tok, Some(Token { kind: '=', .. })) {
buf.push(' ');
// todo: remove this unwrap
buf.push(self.toks.next().unwrap().kind);
if is_angle && self.consume_char_if_exists('=') {
buf.push('=');
}
buf.push(' ');
self.whitespace_or_comment();
buf.push_str(&self.expression_until_comparison()?);
}
self.expect_char(')')?;
self.whitespace_or_comment();
buf.push(')');
Ok(buf)
}
fn parse_single_media_query(&mut self) -> SassResult<String> {
let mut buf = String::new();
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
buf.push_str(&self.parse_identifier()?);
self.whitespace_or_comment();
if let Some(tok) = self.toks.peek() {
if !is_name_start(tok.kind) {
return Ok(buf);
}
}
buf.push(' ');
let ident = self.parse_identifier()?;
self.whitespace_or_comment();
if ident.to_ascii_lowercase() == "and" {
buf.push_str("and ");
} else {
buf.push_str(&ident);
if self.scan_identifier("and", true) {
self.whitespace_or_comment();
buf.push_str(" and ");
} else {
return Ok(buf);
}
}
}
loop {
self.whitespace_or_comment();
buf.push_str(&self.parse_media_feature()?);
self.whitespace_or_comment();
if !self.scan_identifier("and", true) {
break;
}
buf.push_str(" and ");
}
Ok(buf)
}
}

138
src/parse/media_query.rs Normal file
View File

@ -0,0 +1,138 @@
use crate::{ast::MediaQuery, error::SassResult, lexer::Lexer};
use super::BaseParser;
pub(crate) struct MediaQueryParser<'a> {
pub toks: Lexer<'a>,
}
impl<'a> BaseParser<'a> for MediaQueryParser<'a> {
fn toks(&self) -> &Lexer<'a> {
&self.toks
}
fn toks_mut(&mut self) -> &mut Lexer<'a> {
&mut self.toks
}
}
impl<'a> MediaQueryParser<'a> {
pub fn new(toks: Lexer<'a>) -> MediaQueryParser<'a> {
MediaQueryParser { toks }
}
pub fn parse(&mut self) -> SassResult<Vec<MediaQuery>> {
let mut queries = Vec::new();
loop {
self.whitespace()?;
queries.push(self.parse_media_query()?);
self.whitespace()?;
if !self.scan_char(',') {
break;
}
}
if self.toks.next().is_some() {
return Err(("expected no more input.", self.toks.current_span()).into());
}
Ok(queries)
}
fn parse_media_query(&mut self) -> SassResult<MediaQuery> {
if self.toks.next_char_is('(') {
let mut conditions = vec![self.parse_media_in_parens()?];
self.whitespace()?;
let mut conjunction = true;
if self.scan_identifier("and", false)? {
self.expect_whitespace()?;
conditions.append(&mut self.parse_media_logic_sequence("and")?);
} else if self.scan_identifier("or", false)? {
self.expect_whitespace()?;
conjunction = false;
conditions.append(&mut self.parse_media_logic_sequence("or")?);
}
return Ok(MediaQuery::condition(conditions, conjunction));
}
let mut modifier: Option<String> = None;
let media_type: Option<String>;
let identifier1 = self.parse_identifier(false, false)?;
if identifier1.to_ascii_lowercase() == "not" {
self.expect_whitespace()?;
if !self.looking_at_identifier() {
return Ok(MediaQuery::condition(
vec![format!("(not {})", self.parse_media_in_parens()?)],
true,
));
}
}
self.whitespace()?;
if !self.looking_at_identifier() {
return Ok(MediaQuery::media_type(Some(identifier1), None, None));
}
let identifier2 = self.parse_identifier(false, false)?;
if identifier2.to_ascii_lowercase() == "and" {
self.expect_whitespace()?;
media_type = Some(identifier1);
} else {
self.whitespace()?;
modifier = Some(identifier1);
media_type = Some(identifier2);
if self.scan_identifier("and", false)? {
// For example, "@media only screen and ..."
self.expect_whitespace()?;
} else {
// For example, "@media only screen {"
return Ok(MediaQuery::media_type(media_type, modifier, None));
}
}
// We've consumed either `IDENTIFIER "and"` or
// `IDENTIFIER IDENTIFIER "and"`.
if self.scan_identifier("not", false)? {
// For example, "@media screen and not (...) {"
self.expect_whitespace()?;
return Ok(MediaQuery::media_type(
media_type,
modifier,
Some(vec![format!("(not {})", self.parse_media_in_parens()?)]),
));
}
Ok(MediaQuery::media_type(
media_type,
modifier,
Some(self.parse_media_logic_sequence("and")?),
))
}
fn parse_media_in_parens(&mut self) -> SassResult<String> {
self.expect_char('(')?;
let result = format!("({})", self.declaration_value(false)?);
self.expect_char(')')?;
Ok(result)
}
fn parse_media_logic_sequence(&mut self, operator: &'static str) -> SassResult<Vec<String>> {
let mut result = Vec::new();
loop {
result.push(self.parse_media_in_parens()?);
self.whitespace()?;
if !self.scan_identifier(operator, false)? {
return Ok(result);
}
self.expect_whitespace()?;
}
}
}

View File

@ -1,284 +0,0 @@
use std::mem;
use codemap::Spanned;
use crate::{
args::{CallArgs, FuncArgs},
atrule::mixin::{Content, Mixin, UserDefinedMixin},
error::SassResult,
lexer::Lexer,
scope::Scopes,
utils::read_until_closing_curly_brace,
Token,
};
use super::{common::ContextFlags, Parser, Stmt};
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn parse_mixin(&mut self) -> SassResult<()> {
self.whitespace();
let Spanned { node: name, span } = self.parse_identifier_no_interpolation(false)?;
if self.flags.in_mixin() {
return Err(("Mixins may not contain mixin declarations.", span).into());
}
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", span).into());
}
if self.flags.in_control_flow() {
return Err(("Mixins may not be declared in control directives.", span).into());
}
self.whitespace_or_comment();
let args = match self.toks.next() {
Some(Token { kind: '(', .. }) => self.parse_func_args()?,
Some(Token { kind: '{', .. }) => FuncArgs::new(),
Some(t) => return Err(("expected \"{\".", t.pos()).into()),
None => return Err(("expected \"{\".", span).into()),
};
self.whitespace();
let mut body = read_until_closing_curly_brace(self.toks)?;
body.push(match self.toks.next() {
Some(tok) => tok,
None => return Err(("expected \"}\".", self.span_before).into()),
});
// todo: `@include` can only give content when `@content` is present within the body
// if `@content` is *not* present and `@include` attempts to give a body, we throw an error
// `Error: Mixin doesn't accept a content block.`
//
// this is blocked on figuring out just how to check for this. presumably we could have a check
// not when parsing initially, but rather when `@include`ing to see if an `@content` was found.
let mixin = Mixin::new_user_defined(args, body, false, self.at_root);
if self.at_root {
self.global_scope.insert_mixin(name, mixin);
} else {
self.scopes.insert_mixin(name.into(), mixin);
}
Ok(())
}
pub(super) fn parse_include(&mut self) -> SassResult<Vec<Stmt>> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into);
let (mixin, module) = if self.consume_char_if_exists('.') {
let module = name;
let name = self.parse_identifier()?.map_node(Into::into);
(
self.modules
.get(module.node, module.span)?
.get_mixin(name)?,
Some(module),
)
} else {
(self.scopes.get_mixin(name, self.global_scope)?, None)
};
self.whitespace_or_comment();
let args = if self.consume_char_if_exists('(') {
self.parse_call_args()?
} else {
CallArgs::new(name.span)
};
self.whitespace_or_comment();
let content_args = if let Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) =
self.toks.peek()
{
let mut ident = self.parse_identifier_no_interpolation(false)?;
ident.node.make_ascii_lowercase();
if ident.node == "using" {
self.whitespace_or_comment();
self.expect_char('(')?;
Some(self.parse_func_args()?)
} else {
return Err(("expected keyword \"using\".", ident.span).into());
}
} else {
None
};
self.whitespace_or_comment();
let content = if content_args.is_some()
|| matches!(self.toks.peek(), Some(Token { kind: '{', .. }))
{
self.consume_char_if_exists('{');
let mut toks = read_until_closing_curly_brace(self.toks)?;
if let Some(tok) = self.toks.peek() {
toks.push(tok);
self.toks.next();
}
Some(toks)
} else {
None
};
self.consume_char_if_exists(';');
let UserDefinedMixin {
body,
args: fn_args,
declared_at_root,
..
} = match mixin {
Mixin::UserDefined(u) => u,
Mixin::Builtin(b) => {
return b(args, self);
}
};
let scope = self.eval_args(&fn_args, args)?;
let scope_len = self.scopes.len();
if declared_at_root {
mem::swap(self.scopes, self.content_scopes);
}
self.scopes.enter_scope(scope);
if let Some(module) = module {
let module = self.modules.get(module.node, module.span)?;
self.scopes.enter_scope(module.scope.clone());
}
self.content.push(Content {
content,
content_args,
scope_len,
declared_at_root,
});
let body = Parser {
toks: &mut Lexer::new(body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
flags: self.flags | ContextFlags::IN_MIXIN,
content: self.content,
at_root: false,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse_stmt()?;
self.content.pop();
if module.is_some() {
self.scopes.exit_scope();
}
self.scopes.exit_scope();
if declared_at_root {
mem::swap(self.scopes, self.content_scopes);
}
Ok(body)
}
pub(super) fn parse_content_rule(&mut self) -> SassResult<Vec<Stmt>> {
if !self.flags.in_mixin() {
return Err((
"@content is only allowed within mixin declarations.",
self.span_before,
)
.into());
}
Ok(if let Some(content) = self.content.pop() {
let (mut scope_at_decl, mixin_scope) = if content.declared_at_root {
(mem::take(self.content_scopes), Scopes::new())
} else {
mem::take(self.scopes).split_off(content.scope_len)
};
let mut entered_scope = false;
self.whitespace_or_comment();
let call_args = if self.consume_char_if_exists('(') {
self.parse_call_args()?
} else {
CallArgs::new(self.span_before)
};
if let Some(ref content_args) = content.content_args {
call_args.max_args(content_args.len())?;
let scope = self.eval_args(content_args, call_args)?;
scope_at_decl.enter_scope(scope);
entered_scope = true;
} else {
call_args.max_args(0)?;
}
let stmts = if let Some(body) = &content.content {
Parser {
toks: &mut Lexer::new_ref(body),
map: self.map,
path: self.path,
scopes: &mut scope_at_decl,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
flags: self.flags,
content: self.content,
at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse_stmt()?
} else {
Vec::new()
};
if entered_scope {
scope_at_decl.exit_scope();
}
scope_at_decl.merge(mixin_scope);
if content.declared_at_root {
*self.content_scopes = scope_at_decl;
} else {
*self.scopes = scope_at_decl;
}
self.content.push(content);
stmts
} else {
Vec::new()
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,305 +0,0 @@
use std::convert::TryFrom;
use codemap::Spanned;
use crate::{
atrule::AtRuleKind,
builtin::modules::{
declare_module_color, declare_module_list, declare_module_map, declare_module_math,
declare_module_meta, declare_module_selector, declare_module_string, Module, ModuleConfig,
Modules,
},
common::Identifier,
error::SassResult,
lexer::Lexer,
parse::{common::Comment, Parser, Stmt, VariableValue},
scope::Scope,
Token,
};
impl<'a, 'b> Parser<'a, 'b> {
fn parse_module_alias(&mut self) -> SassResult<Option<String>> {
if !matches!(
self.toks.peek(),
Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. })
) {
return Ok(None);
}
let mut ident = self.parse_identifier_no_interpolation(false)?;
ident.node.make_ascii_lowercase();
if ident.node != "as" {
return Err(("expected \";\".", ident.span).into());
}
self.whitespace_or_comment();
if self.consume_char_if_exists('*') {
return Ok(Some('*'.to_string()));
}
let name = self.parse_identifier_no_interpolation(false)?;
Ok(Some(name.node))
}
fn parse_module_config(&mut self) -> SassResult<ModuleConfig> {
let mut config = ModuleConfig::default();
if !matches!(
self.toks.peek(),
Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. })
) {
return Ok(config);
}
let mut ident = self.parse_identifier_no_interpolation(false)?;
ident.node.make_ascii_lowercase();
if ident.node != "with" {
return Err(("expected \";\".", ident.span).into());
}
self.whitespace_or_comment();
self.span_before = ident.span;
self.expect_char('(')?;
loop {
self.whitespace_or_comment();
self.expect_char('$')?;
let name = self.parse_identifier_no_interpolation(false)?;
self.whitespace_or_comment();
self.expect_char(':')?;
self.whitespace_or_comment();
let value = self.parse_value(false, &|parser| {
matches!(
parser.toks.peek(),
Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. })
)
})?;
config.insert(name.map_node(Into::into), value)?;
match self.toks.next() {
Some(Token { kind: ',', .. }) => {
continue;
}
Some(Token { kind: ')', .. }) => {
break;
}
Some(..) | None => {
return Err(("expected \")\".", self.span_before).into());
}
}
}
Ok(config)
}
pub fn load_module(
&mut self,
name: &str,
config: &mut ModuleConfig,
) -> SassResult<(Module, Vec<Stmt>)> {
Ok(match name {
"sass:color" => (declare_module_color(), Vec::new()),
"sass:list" => (declare_module_list(), Vec::new()),
"sass:map" => (declare_module_map(), Vec::new()),
"sass:math" => (declare_module_math(), Vec::new()),
"sass:meta" => (declare_module_meta(), Vec::new()),
"sass:selector" => (declare_module_selector(), Vec::new()),
"sass:string" => (declare_module_string(), Vec::new()),
_ => {
if let Some(import) = self.find_import(name.as_ref()) {
let mut global_scope = Scope::new();
let file = self.map.add_file(
name.to_owned(),
String::from_utf8(self.options.fs.read(&import)?)?,
);
let mut modules = Modules::default();
let stmts = Parser {
toks: &mut Lexer::new_from_file(&file),
map: self.map,
path: &import,
scopes: self.scopes,
global_scope: &mut global_scope,
super_selectors: self.super_selectors,
span_before: file.span.subspan(0, 0),
content: self.content,
flags: self.flags,
at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: &mut modules,
module_config: config,
}
.parse()?;
if !config.is_empty() {
return Err((
"This variable was not declared with !default in the @used module.",
self.span_before,
)
.into());
}
(Module::new_from_scope(global_scope, modules, false), stmts)
} else {
return Err(("Can't find stylesheet to import.", self.span_before).into());
}
}
})
}
/// Returns any multiline comments that may have been found
/// while loading modules
pub(super) fn load_modules(&mut self) -> SassResult<Vec<Stmt>> {
let mut comments = Vec::new();
loop {
self.whitespace();
match self.toks.peek() {
Some(Token { kind: '@', .. }) => {
let start = self.toks.cursor();
self.toks.next();
if let Some(Token { kind, .. }) = self.toks.peek() {
if !matches!(kind, 'u' | 'U' | '\\') {
self.toks.set_cursor(start);
break;
}
}
let ident = self.parse_identifier_no_interpolation(false)?;
if AtRuleKind::try_from(&ident)? != AtRuleKind::Use {
self.toks.set_cursor(start);
break;
}
self.whitespace_or_comment();
let quote = match self.toks.next() {
Some(Token { kind: q @ '"', .. }) | Some(Token { kind: q @ '\'', .. }) => q,
Some(..) | None => {
return Err(("Expected string.", self.span_before).into())
}
};
let Spanned { node: module, span } = self.parse_quoted_string(quote)?;
let module_name = module
.unquote()
.to_css_string(span, self.options.is_compressed())?;
self.whitespace_or_comment();
let module_alias = self.parse_module_alias()?;
self.whitespace_or_comment();
let mut config = self.parse_module_config()?;
self.whitespace_or_comment();
self.expect_char(';')?;
let (module, mut stmts) =
self.load_module(module_name.as_ref(), &mut config)?;
comments.append(&mut stmts);
// if the config isn't empty here, that means
// variables were passed to a builtin module
if !config.is_empty() {
return Err(("Built-in modules can't be configured.", span).into());
}
let module_name = match module_alias.as_deref() {
Some("*") => {
self.modules.merge(module.modules);
self.global_scope.merge_module_scope(module.scope);
continue;
}
Some(..) => module_alias.unwrap(),
None => match module_name.as_ref() {
"sass:color" => "color".to_owned(),
"sass:list" => "list".to_owned(),
"sass:map" => "map".to_owned(),
"sass:math" => "math".to_owned(),
"sass:meta" => "meta".to_owned(),
"sass:selector" => "selector".to_owned(),
"sass:string" => "string".to_owned(),
_ => module_name.into_owned(),
},
};
self.modules.insert(module_name.into(), module, span)?;
}
Some(Token { kind: '/', .. }) => {
self.toks.next();
match self.parse_comment()?.node {
Comment::Silent => continue,
Comment::Loud(s) => comments.push(Stmt::Comment(s)),
}
}
Some(Token { kind: '$', .. }) => self.parse_variable_declaration()?,
Some(..) | None => break,
}
}
self.toks.reset_cursor();
Ok(comments)
}
pub(super) fn parse_module_variable_redeclaration(
&mut self,
module: Identifier,
) -> SassResult<()> {
let variable = self
.parse_identifier_no_interpolation(false)?
.map_node(Into::into);
self.whitespace_or_comment();
self.expect_char(':')?;
let VariableValue {
var_value,
global,
default,
} = self.parse_variable_value()?;
if global {
return Err((
"!global isn't allowed for variables in other modules.",
variable.span,
)
.into());
}
if default {
return Ok(());
}
let value = var_value?;
self.modules
.get_mut(module, variable.span)?
.update_var(variable, value.node)?;
Ok(())
}
}

566
src/parse/sass.rs Normal file
View File

@ -0,0 +1,566 @@
use std::path::Path;
use codemap::{CodeMap, Span};
use crate::{ast::*, error::SassResult, lexer::Lexer, token::Token, ContextFlags, Options};
use super::{BaseParser, StylesheetParser};
pub(crate) struct SassParser<'a> {
pub toks: Lexer<'a>,
// todo: likely superfluous
pub map: &'a mut CodeMap,
pub path: &'a Path,
pub span_before: Span,
pub flags: ContextFlags,
pub options: &'a Options<'a>,
pub current_indentation: usize,
pub next_indentation: Option<usize>,
pub spaces: Option<bool>,
pub next_indentation_end: Option<usize>,
}
impl<'a> BaseParser<'a> for SassParser<'a> {
fn toks(&self) -> &Lexer<'a> {
&self.toks
}
fn toks_mut(&mut self) -> &mut Lexer<'a> {
&mut self.toks
}
fn whitespace_without_comments(&mut self) {
while let Some(next) = self.toks.peek() {
if next.kind != '\t' && next.kind != ' ' {
break;
}
self.toks.next();
}
}
fn skip_loud_comment(&mut self) -> SassResult<()> {
self.expect_char('/')?;
self.expect_char('*')?;
loop {
let mut next = self.toks.next();
match next {
Some(Token { kind: '\n', .. }) => {
return Err(("expected */.", self.toks.prev_span()).into())
}
Some(Token { kind: '*', .. }) => {}
_ => continue,
}
loop {
next = self.toks.next();
if !matches!(next, Some(Token { kind: '*', .. })) {
break;
}
}
if matches!(next, Some(Token { kind: '/', .. })) {
break;
}
}
Ok(())
}
}
impl<'a> StylesheetParser<'a> for SassParser<'a> {
fn is_plain_css(&mut self) -> bool {
false
}
fn is_indented(&mut self) -> bool {
true
}
fn path(&mut self) -> &'a Path {
self.path
}
fn map(&mut self) -> &mut CodeMap {
self.map
}
fn options(&self) -> &Options {
self.options
}
fn flags(&mut self) -> &ContextFlags {
&self.flags
}
fn flags_mut(&mut self) -> &mut ContextFlags {
&mut self.flags
}
fn current_indentation(&self) -> usize {
self.current_indentation
}
fn span_before(&self) -> Span {
self.span_before
}
fn parse_style_rule_selector(&mut self) -> SassResult<Interpolation> {
let mut buffer = Interpolation::new();
loop {
buffer.add_interpolation(self.almost_any_value(true)?);
buffer.add_char('\n');
if !(buffer.trailing_string().trim_end().ends_with(',') && self.scan_char('\n')) {
break;
}
}
Ok(buffer)
}
fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> {
if !self.at_end_of_statement() {
self.expect_newline()?;
}
if self.peek_indentation()? <= self.current_indentation {
return Ok(());
}
// todo: position: _nextIndentationEnd!.position
// todo: error message, "Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}."
Err(("Nothing may be indented here", self.toks.current_span()).into())
}
fn at_end_of_statement(&self) -> bool {
matches!(self.toks.peek(), Some(Token { kind: '\n', .. }) | None)
}
fn looking_at_children(&mut self) -> SassResult<bool> {
Ok(self.at_end_of_statement() && self.peek_indentation()? > self.current_indentation)
}
fn scan_else(&mut self, if_indentation: usize) -> SassResult<bool> {
if self.peek_indentation()? != if_indentation {
return Ok(false);
}
let start = self.toks.cursor();
let start_indentation = self.current_indentation;
let start_next_indentation = self.next_indentation;
let start_next_indentation_end = self.next_indentation_end;
self.read_indentation()?;
if self.scan_char('@') && self.scan_identifier("else", false)? {
return Ok(true);
}
self.toks.set_cursor(start);
self.current_indentation = start_indentation;
self.next_indentation = start_next_indentation;
self.next_indentation_end = start_next_indentation_end;
Ok(false)
}
fn parse_children(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<Vec<AstStmt>> {
let mut children = Vec::new();
self.while_indented_lower(|parser| {
if let Some(parsed_child) = parser.parse_child(|parser| Ok(Some(child(parser)?)))? {
children.push(parsed_child);
}
Ok(())
})?;
Ok(children)
}
fn parse_statements(
&mut self,
statement: fn(&mut Self) -> SassResult<Option<AstStmt>>,
) -> SassResult<Vec<AstStmt>> {
if self.toks.next_char_is(' ') || self.toks.next_char_is('\t') {
return Err((
"Indenting at the beginning of the document is illegal.",
self.toks.current_span(),
)
.into());
}
let mut statements = Vec::new();
while self.toks.peek().is_some() {
if let Some(child) = self.parse_child(statement)? {
statements.push(child);
}
let indentation = self.read_indentation()?;
assert_eq!(indentation, 0);
}
Ok(statements)
}
fn parse_silent_comment(&mut self) -> SassResult<AstStmt> {
let start = self.toks.cursor();
self.expect_char('/')?;
self.expect_char('/')?;
let mut buffer = String::new();
let parent_indentation = self.current_indentation;
'outer: loop {
let comment_prefix = if self.scan_char('/') { "///" } else { "//" };
loop {
buffer.push_str(comment_prefix);
// buffer.write(commentPrefix);
// Skip the initial characters because we're already writing the
// slashes.
for _ in comment_prefix.len()..(self.current_indentation - parent_indentation) {
buffer.push(' ');
}
while self.toks.peek().is_some() && !self.toks.next_char_is('\n') {
buffer.push(self.toks.next().unwrap().kind);
}
buffer.push('\n');
if self.peek_indentation()? < parent_indentation {
break 'outer;
}
if self.peek_indentation()? == parent_indentation {
// Look ahead to the next line to see if it starts another comment.
if matches!(
self.toks.peek_n(1 + parent_indentation),
Some(Token { kind: '/', .. })
) && matches!(
self.toks.peek_n(2 + parent_indentation),
Some(Token { kind: '/', .. })
) {
self.read_indentation()?;
}
break;
}
self.read_indentation()?;
}
if !self.scan("//") {
break;
}
}
Ok(AstStmt::SilentComment(AstSilentComment {
text: buffer,
span: self.toks.span_from(start),
}))
}
fn parse_loud_comment(&mut self) -> SassResult<AstLoudComment> {
let start = self.toks.cursor();
self.expect_char('/')?;
self.expect_char('*')?;
let mut first = true;
let mut buffer = Interpolation::new_plain("/*".to_owned());
let parent_indentation = self.current_indentation;
loop {
if first {
let beginning_of_comment = self.toks.cursor();
self.spaces();
if self.toks.next_char_is('\n') {
self.read_indentation()?;
buffer.add_char(' ');
} else {
buffer.add_string(self.toks.raw_text(beginning_of_comment));
}
} else {
buffer.add_string("\n * ".to_owned());
}
first = false;
for _ in 3..(self.current_indentation - parent_indentation) {
buffer.add_char(' ');
}
while self.toks.peek().is_some() {
match self.toks.peek() {
Some(Token {
kind: '\n' | '\r', ..
}) => break,
Some(Token { kind: '#', .. }) => {
if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) {
buffer.add_interpolation(self.parse_single_interpolation()?);
} else {
buffer.add_char('#');
self.toks.next();
}
}
Some(Token { kind, .. }) => {
buffer.add_char(kind);
self.toks.next();
}
None => todo!(),
}
}
if self.peek_indentation()? <= parent_indentation {
break;
}
// Preserve empty lines.
while self.looking_at_double_newline() {
self.expect_newline()?;
buffer.add_char('\n');
buffer.add_char(' ');
buffer.add_char('*');
}
self.read_indentation()?;
}
if !buffer.trailing_string().trim_end().ends_with("*/") {
buffer.add_string(" */".to_owned());
}
Ok(AstLoudComment {
text: buffer,
span: self.toks.span_from(start),
})
}
}
impl<'a> SassParser<'a> {
pub fn new(
toks: Lexer<'a>,
map: &'a mut CodeMap,
options: &'a Options<'a>,
span_before: Span,
file_name: &'a Path,
) -> Self {
let mut flags = ContextFlags::empty();
flags.set(ContextFlags::IS_USE_ALLOWED, true);
SassParser {
toks,
map,
path: file_name,
span_before,
flags,
options,
current_indentation: 0,
next_indentation: None,
next_indentation_end: None,
spaces: None,
}
}
fn peek_indentation(&mut self) -> SassResult<usize> {
if let Some(next) = self.next_indentation {
return Ok(next);
}
if self.toks.peek().is_none() {
self.next_indentation = Some(0);
self.next_indentation_end = Some(self.toks.cursor());
return Ok(0);
}
let start = self.toks.cursor();
if !self.scan_char('\n') {
return Err(("Expected newline.", self.toks.current_span()).into());
}
let mut contains_tab;
let mut contains_space;
let mut next_indentation;
loop {
contains_tab = false;
contains_space = false;
next_indentation = 0;
while let Some(next) = self.toks.peek() {
match next.kind {
' ' => contains_space = true,
'\t' => contains_tab = true,
_ => break,
}
next_indentation += 1;
self.toks.next();
}
if self.toks.peek().is_none() {
self.next_indentation = Some(0);
self.next_indentation_end = Some(self.toks.cursor());
self.toks.set_cursor(start);
return Ok(0);
}
if !self.scan_char('\n') {
break;
}
}
self.check_indentation_consistency(contains_tab, contains_space, start)?;
self.next_indentation = Some(next_indentation);
if next_indentation > 0 {
self.spaces.get_or_insert(contains_space);
}
self.next_indentation_end = Some(self.toks.cursor());
self.toks.set_cursor(start);
Ok(next_indentation)
}
fn check_indentation_consistency(
&mut self,
contains_tab: bool,
contains_space: bool,
start: usize,
) -> SassResult<()> {
// NOTE: error message spans here start from the beginning of the line
if contains_tab {
if contains_space {
return Err((
"Tabs and spaces may not be mixed.",
self.toks.span_from(start),
)
.into());
} else if self.spaces == Some(true) {
return Err(("Expected spaces, was tabs.", self.toks.span_from(start)).into());
}
} else if contains_space && self.spaces == Some(false) {
return Err(("Expected tabs, was spaces.", self.toks.span_from(start)).into());
}
Ok(())
}
fn expect_newline(&mut self) -> SassResult<()> {
match self.toks.peek() {
Some(Token { kind: ';', .. }) => Err((
"semicolons aren't allowed in the indented syntax.",
self.toks.current_span(),
)
.into()),
Some(Token { kind: '\r', .. }) => {
self.toks.next();
self.scan_char('\n');
Ok(())
}
Some(Token { kind: '\n', .. }) => {
self.toks.next();
Ok(())
}
_ => Err(("expected newline.", self.toks.current_span()).into()),
}
}
fn read_indentation(&mut self) -> SassResult<usize> {
self.current_indentation = match self.next_indentation {
Some(indent) => indent,
None => {
let indent = self.peek_indentation()?;
self.next_indentation = Some(indent);
indent
}
};
self.toks.set_cursor(self.next_indentation_end.unwrap());
self.next_indentation = None;
self.next_indentation_end = None;
Ok(self.current_indentation)
}
fn while_indented_lower(
&mut self,
mut body: impl FnMut(&mut Self) -> SassResult<()>,
) -> SassResult<()> {
let parent_indentation = self.current_indentation;
let mut child_indentation = None;
while self.peek_indentation()? > parent_indentation {
let indentation = self.read_indentation()?;
let child_indent = *child_indentation.get_or_insert(indentation);
if child_indent != indentation {
return Err((
format!("Inconsistent indentation, expected {child_indent} spaces."),
self.toks.current_span(),
)
.into());
}
body(self)?;
}
Ok(())
}
fn parse_child(
&mut self,
child: impl FnOnce(&mut Self) -> SassResult<Option<AstStmt>>,
) -> SassResult<Option<AstStmt>> {
Ok(Some(match self.toks.peek() {
Some(Token {
kind: '\n' | '\r', ..
}) => return Ok(None),
Some(Token { kind: '$', .. }) => AstStmt::VariableDecl(
self.parse_variable_declaration_without_namespace(None, None)?,
),
Some(Token { kind: '/', .. }) => match self.toks.peek_n(1) {
Some(Token { kind: '/', .. }) => self.parse_silent_comment()?,
Some(Token { kind: '*', .. }) => AstStmt::LoudComment(self.parse_loud_comment()?),
_ => return child(self),
},
_ => return child(self),
}))
}
fn looking_at_double_newline(&mut self) -> bool {
match self.toks.peek() {
// todo: is this branch reachable
Some(Token { kind: '\r', .. }) => match self.toks.peek_n(1) {
Some(Token { kind: '\n', .. }) => {
matches!(self.toks.peek_n(2), Some(Token { kind: '\n', .. }))
}
Some(Token { kind: '\r', .. }) => true,
_ => false,
},
Some(Token { kind: '\n', .. }) => matches!(
self.toks.peek_n(1),
Some(Token {
kind: '\n' | '\r',
..
})
),
_ => false,
}
}
}

88
src/parse/scss.rs Normal file
View File

@ -0,0 +1,88 @@
use std::path::Path;
use codemap::{CodeMap, Span};
use crate::{lexer::Lexer, ContextFlags, Options};
use super::{BaseParser, StylesheetParser};
pub(crate) struct ScssParser<'a> {
pub toks: Lexer<'a>,
// todo: likely superfluous
pub map: &'a mut CodeMap,
pub path: &'a Path,
pub span_before: Span,
pub flags: ContextFlags,
pub options: &'a Options<'a>,
}
impl<'a> ScssParser<'a> {
pub fn new(
toks: Lexer<'a>,
map: &'a mut CodeMap,
options: &'a Options<'a>,
span_before: Span,
file_name: &'a Path,
) -> Self {
let mut flags = ContextFlags::empty();
flags.set(ContextFlags::IS_USE_ALLOWED, true);
ScssParser {
toks,
map,
path: file_name,
span_before,
flags,
options,
}
}
}
impl<'a> BaseParser<'a> for ScssParser<'a> {
fn toks(&self) -> &Lexer<'a> {
&self.toks
}
fn toks_mut(&mut self) -> &mut Lexer<'a> {
&mut self.toks
}
}
impl<'a> StylesheetParser<'a> for ScssParser<'a> {
fn is_plain_css(&mut self) -> bool {
false
}
fn is_indented(&mut self) -> bool {
false
}
fn path(&mut self) -> &'a Path {
self.path
}
fn map(&mut self) -> &mut CodeMap {
self.map
}
fn options(&self) -> &Options {
self.options
}
fn current_indentation(&self) -> usize {
0
}
fn flags(&mut self) -> &ContextFlags {
&self.flags
}
fn flags_mut(&mut self) -> &mut ContextFlags {
&mut self.flags
}
fn span_before(&self) -> Span {
self.span_before
}
}

View File

@ -1,290 +0,0 @@
use codemap::Spanned;
use crate::{
error::SassResult,
interner::InternedString,
style::Style,
utils::{is_name, is_name_start},
value::Value,
Token,
};
use super::common::SelectorOrStyle;
use super::Parser;
impl<'a, 'b> Parser<'a, 'b> {
fn parse_style_value_when_no_space_after_semicolon(&mut self) -> Option<Vec<Token>> {
let mut toks = Vec::new();
while let Some(tok) = self.toks.peek() {
match tok.kind {
';' | '}' => {
self.toks.reset_cursor();
break;
}
'{' => {
self.toks.reset_cursor();
return None;
}
'(' => {
toks.push(tok);
self.toks.peek_forward(1);
let mut scope = 0;
while let Some(tok) = self.toks.peek() {
match tok.kind {
')' => {
if scope == 0 {
toks.push(tok);
self.toks.peek_forward(1);
break;
}
scope -= 1;
toks.push(tok);
self.toks.peek_forward(1);
}
'(' => {
toks.push(tok);
self.toks.peek_forward(1);
scope += 1;
}
_ => {
toks.push(tok);
self.toks.peek_forward(1);
}
}
}
}
_ => {
toks.push(tok);
self.toks.peek_forward(1);
}
}
}
Some(toks)
}
/// Determines whether the parser is looking at a style or a selector
///
/// When parsing the children of a style rule, property declarations,
/// namespaced variable declarations, and nested style rules can all begin
/// with bare identifiers. In order to know which statement type to produce,
/// we need to disambiguate them. We use the following criteria:
///
/// * If the entity starts with an identifier followed by a period and a
/// dollar sign, it's a variable declaration. This is the simplest case,
/// because `.$` is used in and only in variable declarations.
///
/// * If the entity doesn't start with an identifier followed by a colon,
/// it's a selector. There are some additional mostly-unimportant cases
/// here to support various declaration hacks.
///
/// * If the colon is followed by another colon, it's a selector.
///
/// * Otherwise, if the colon is followed by anything other than
/// interpolation or a character that's valid as the beginning of an
/// identifier, it's a declaration.
///
/// * If the colon is followed by interpolation or a valid identifier, try
/// parsing it as a declaration value. If this fails, backtrack and parse
/// it as a selector.
///
/// * If the declaration value is valid but is followed by "{", backtrack and
/// parse it as a selector anyway. This ensures that ".foo:bar {" is always
/// parsed as a selector and never as a property with nested properties
/// beneath it.
// todo: potentially we read the property to a string already since properties
// are more common than selectors? this seems to be annihilating our performance
pub(super) fn is_selector_or_style(&mut self) -> SassResult<SelectorOrStyle> {
if let Some(first_char) = self.toks.peek() {
if first_char.kind == '#' {
if !matches!(self.toks.peek_forward(1), Some(Token { kind: '{', .. })) {
self.toks.reset_cursor();
return Ok(SelectorOrStyle::Selector(String::new()));
}
self.toks.reset_cursor();
} else if !is_name_start(first_char.kind) && first_char.kind != '-' {
return Ok(SelectorOrStyle::Selector(String::new()));
}
}
let mut property = self.parse_identifier()?.node;
let whitespace_after_property = self.whitespace_or_comment();
match self.toks.peek() {
Some(Token { kind: ':', .. }) => {
self.toks.next();
if let Some(Token { kind, .. }) = self.toks.peek() {
return Ok(match kind {
':' => {
if whitespace_after_property {
property.push(' ');
}
property.push(':');
SelectorOrStyle::Selector(property)
}
c if is_name(c) => {
if let Some(toks) =
self.parse_style_value_when_no_space_after_semicolon()
{
let len = toks.len();
if let Ok(val) = self.parse_value_from_vec(&toks, false) {
self.toks.take(len).for_each(drop);
return Ok(SelectorOrStyle::Style(
InternedString::get_or_intern(property),
Some(Box::new(val)),
));
}
}
if whitespace_after_property {
property.push(' ');
}
property.push(':');
return Ok(SelectorOrStyle::Selector(property));
}
_ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None),
});
}
}
Some(Token { kind: '.', .. }) => {
if matches!(self.toks.peek_next(), Some(Token { kind: '$', .. })) {
self.toks.next();
self.toks.next();
return Ok(SelectorOrStyle::ModuleVariableRedeclaration(
property.into(),
));
}
if whitespace_after_property {
property.push(' ');
}
return Ok(SelectorOrStyle::Selector(property));
}
_ => {
if whitespace_after_property {
property.push(' ');
}
return Ok(SelectorOrStyle::Selector(property));
}
}
Err(("expected \"{\".", self.span_before).into())
}
fn parse_property(&mut self, mut super_property: String) -> SassResult<String> {
let property = self.parse_identifier()?;
self.whitespace_or_comment();
// todo: expect_char(':')?;
if self.consume_char_if_exists(':') {
self.whitespace_or_comment();
} else {
return Err(("Expected \":\".", property.span).into());
}
if super_property.is_empty() {
Ok(property.node)
} else {
super_property.reserve(1 + property.node.len());
super_property.push('-');
super_property.push_str(&property.node);
Ok(super_property)
}
}
fn parse_style_value(&mut self) -> SassResult<Spanned<Value>> {
self.parse_value(false, &|_| false)
}
pub(super) fn parse_style_group(
&mut self,
super_property: InternedString,
) -> SassResult<Vec<Style>> {
let mut styles = Vec::new();
self.whitespace();
while let Some(tok) = self.toks.peek() {
match tok.kind {
'{' => {
self.toks.next();
self.whitespace();
loop {
let property = InternedString::get_or_intern(
self.parse_property(super_property.resolve())?,
);
if let Some(tok) = self.toks.peek() {
if tok.kind == '{' {
styles.append(&mut self.parse_style_group(property)?);
self.whitespace();
if let Some(tok) = self.toks.peek() {
if tok.kind == '}' {
self.toks.next();
self.whitespace();
return Ok(styles);
}
continue;
}
continue;
}
}
let value = Box::new(self.parse_style_value()?);
match self.toks.peek() {
Some(Token { kind: '}', .. }) => {
styles.push(Style { property, value });
}
Some(Token { kind: ';', .. }) => {
self.toks.next();
self.whitespace();
styles.push(Style { property, value });
}
Some(Token { kind: '{', .. }) => {
styles.push(Style { property, value });
styles.append(&mut self.parse_style_group(property)?);
}
Some(..) | None => {
self.whitespace();
styles.push(Style { property, value });
}
}
if let Some(tok) = self.toks.peek() {
match tok.kind {
'}' => {
self.toks.next();
self.whitespace();
return Ok(styles);
}
_ => continue,
}
}
}
}
_ => {
let value = self.parse_style_value()?;
let t = self
.toks
.peek()
.ok_or(("expected more input.", value.span))?;
match t.kind {
';' => {
self.toks.next();
self.whitespace();
}
'{' => {
let mut v = vec![Style {
property: super_property,
value: Box::new(value),
}];
v.append(&mut self.parse_style_group(super_property)?);
return Ok(v);
}
_ => {}
}
return Ok(vec![Style {
property: super_property,
value: Box::new(value),
}]);
}
}
}
Ok(styles)
}
}

3058
src/parse/stylesheet.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +0,0 @@
//! Consume tokens without allocating
use crate::{error::SassResult, Token};
use super::Parser;
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn throw_away_until_newline(&mut self) {
for tok in &mut self.toks {
if tok.kind == '\n' {
break;
}
}
}
pub(super) fn throw_away_quoted_string(&mut self, q: char) -> SassResult<()> {
while let Some(tok) = self.toks.next() {
match tok.kind {
'"' if q == '"' => {
return Ok(());
}
'\'' if q == '\'' => {
return Ok(());
}
'\\' => {
if self.toks.next().is_none() {
return Err((format!("Expected {}.", q), tok.pos).into());
}
}
'#' => match self.toks.peek() {
Some(Token { kind: '{', .. }) => {
self.toks.next();
self.throw_away_until_closing_curly_brace()?;
}
Some(..) => {}
None => return Err(("expected \"{\".", self.span_before).into()),
},
_ => {}
}
}
Err((format!("Expected {}.", q), self.span_before).into())
}
pub(super) fn throw_away_until_open_curly_brace(&mut self) -> SassResult<()> {
while let Some(tok) = self.toks.next() {
match tok.kind {
'{' => return Ok(()),
'/' => {
match self.toks.peek() {
Some(Token { kind: '/', .. }) => self.throw_away_until_newline(),
_ => {}
};
continue;
}
'\\' | '#' => {
self.toks.next();
}
q @ '"' | q @ '\'' => {
self.throw_away_quoted_string(q)?;
continue;
}
_ => {}
}
}
Err(("expected \"{\".", self.span_before).into())
}
pub(super) fn throw_away_until_closing_curly_brace(&mut self) -> SassResult<()> {
let mut nesting = 0;
while let Some(tok) = self.toks.next() {
match tok.kind {
q @ '"' | q @ '\'' => {
self.throw_away_quoted_string(q)?;
}
'{' => {
nesting += 1;
}
'}' => {
if nesting == 0 {
return Ok(());
}
nesting -= 1;
}
'/' => match self.toks.peek() {
Some(Token { kind: '/', .. }) => {
self.throw_away_until_newline();
}
Some(..) | None => continue,
},
'(' => {
self.throw_away_until_closing_paren()?;
}
'\\' => {
self.toks.next();
}
_ => {}
}
}
Err(("expected \"}\".", self.span_before).into())
}
pub(super) fn throw_away_until_closing_paren(&mut self) -> SassResult<()> {
let mut scope = 0;
while let Some(tok) = self.toks.next() {
match tok.kind {
')' => {
if scope < 1 {
return Ok(());
}
scope -= 1;
}
'(' => scope += 1,
'"' | '\'' => {
self.throw_away_quoted_string(tok.kind)?;
}
'\\' => {
match self.toks.next() {
Some(tok) => tok,
None => continue,
};
}
_ => {}
}
}
Err(("expected \")\".", self.span_before).into())
}
}

1826
src/parse/value.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,466 +0,0 @@
use std::{borrow::Borrow, iter::Iterator};
use crate::{error::SassResult, parse::common::Comment, utils::IsWhitespace, value::Value, Token};
use super::super::Parser;
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn parse_calc_args(&mut self, buf: &mut String) -> SassResult<()> {
buf.reserve(2);
buf.push('(');
let mut nesting = 0;
while let Some(tok) = self.toks.next() {
match tok.kind {
' ' | '\t' | '\n' => {
self.whitespace();
buf.push(' ');
}
'#' => {
if let Some(Token { kind: '{', pos }) = self.toks.peek() {
self.span_before = pos;
self.toks.next();
let interpolation = self.parse_interpolation()?;
buf.push_str(
&interpolation
.node
.to_css_string(interpolation.span, self.options.is_compressed())?,
);
} else {
buf.push('#');
}
}
'(' => {
nesting += 1;
buf.push('(');
}
')' => {
if nesting == 0 {
break;
}
nesting -= 1;
buf.push(')');
}
q @ '\'' | q @ '"' => {
buf.push('"');
match self.parse_quoted_string(q)?.node {
Value::String(ref s, ..) => buf.push_str(s),
_ => unreachable!(),
}
buf.push('"');
}
c => buf.push(c),
}
}
buf.push(')');
Ok(())
}
pub(super) fn parse_progid(&mut self) -> SassResult<String> {
let mut string = String::new();
let mut span = match self.toks.peek() {
Some(token) => token.pos(),
None => {
return Err(("expected \"(\".", self.span_before).into());
}
};
while let Some(tok) = self.toks.next() {
span = span.merge(tok.pos());
match tok.kind {
'a'..='z' | 'A'..='Z' | '.' => {
string.push(tok.kind);
}
'(' => {
self.parse_calc_args(&mut string)?;
break;
}
_ => return Err(("expected \"(\".", span).into()),
}
}
Ok(string)
}
pub(super) fn try_parse_url(&mut self) -> SassResult<Option<String>> {
let mut buf = String::from("url(");
let start = self.toks.cursor();
self.whitespace();
while let Some(tok) = self.toks.next() {
match tok.kind {
'!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => buf.push(tok.kind),
'#' => {
if self.consume_char_if_exists('{') {
let interpolation = self.parse_interpolation()?;
match interpolation.node {
Value::String(ref s, ..) => buf.push_str(s),
v => buf.push_str(
v.to_css_string(interpolation.span, self.options.is_compressed())?
.borrow(),
),
};
} else {
buf.push('#');
}
}
')' => {
buf.push(')');
return Ok(Some(buf));
}
' ' | '\t' | '\n' | '\r' => {
self.whitespace();
if self.consume_char_if_exists(')') {
buf.push(')');
return Ok(Some(buf));
}
break;
}
_ => break,
}
}
self.toks.set_cursor(start);
Ok(None)
}
pub(super) fn try_parse_min_max(
&mut self,
fn_name: &str,
allow_comma: bool,
) -> SassResult<Option<String>> {
let mut buf = if allow_comma {
format!("{}(", fn_name)
} else {
String::new()
};
self.whitespace_or_comment();
while let Some(tok) = self.toks.peek() {
let kind = tok.kind;
match kind {
'+' | '-' | '0'..='9' => {
let number = self.parse_dimension(&|_| false)?;
buf.push_str(
&number
.node
.to_css_string(number.span, self.options.is_compressed())?,
);
}
'#' => {
self.toks.next();
if self.consume_char_if_exists('{') {
let interpolation = self.parse_interpolation_as_string()?;
buf.push_str(&interpolation);
} else {
return Ok(None);
}
}
'c' | 'C' => {
if let Some(name) = self.try_parse_min_max_function("calc")? {
buf.push_str(&name);
} else {
return Ok(None);
}
}
'e' | 'E' => {
if let Some(name) = self.try_parse_min_max_function("env")? {
buf.push_str(&name);
} else {
return Ok(None);
}
}
'v' | 'V' => {
if let Some(name) = self.try_parse_min_max_function("var")? {
buf.push_str(&name);
} else {
return Ok(None);
}
}
'(' => {
self.toks.next();
buf.push('(');
if let Some(val) = self.try_parse_min_max(fn_name, false)? {
buf.push_str(&val);
} else {
return Ok(None);
}
}
'm' | 'M' => {
self.toks.next();
let inner_fn_name = match self.toks.peek() {
Some(Token { kind: 'i', .. }) | Some(Token { kind: 'I', .. }) => {
self.toks.next();
if !matches!(
self.toks.peek(),
Some(Token { kind: 'n', .. }) | Some(Token { kind: 'N', .. })
) {
return Ok(None);
}
"min"
}
Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) => {
self.toks.next();
if !matches!(
self.toks.peek(),
Some(Token { kind: 'x', .. }) | Some(Token { kind: 'X', .. })
) {
return Ok(None);
}
"max"
}
_ => return Ok(None),
};
self.toks.next();
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
return Ok(None);
}
self.toks.next();
if let Some(val) = self.try_parse_min_max(inner_fn_name, true)? {
buf.push_str(&val);
} else {
return Ok(None);
}
}
_ => return Ok(None),
}
self.whitespace_or_comment();
let next = match self.toks.peek() {
Some(tok) => tok,
None => return Ok(None),
};
match next.kind {
')' => {
self.toks.next();
buf.push(')');
return Ok(Some(buf));
}
'+' | '-' | '*' | '/' => {
self.toks.next();
buf.push(' ');
buf.push(next.kind);
buf.push(' ');
}
',' => {
if !allow_comma {
return Ok(None);
}
self.toks.next();
buf.push(',');
buf.push(' ');
}
_ => return Ok(None),
}
self.whitespace_or_comment();
}
Ok(Some(buf))
}
fn try_parse_min_max_function(&mut self, fn_name: &'static str) -> SassResult<Option<String>> {
let mut ident = self.parse_identifier_no_interpolation(false)?.node;
ident.make_ascii_lowercase();
if ident != fn_name {
return Ok(None);
}
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
return Ok(None);
}
self.toks.next();
ident.push('(');
let value = self.declaration_value(true, false, true)?;
if !matches!(self.toks.peek(), Some(Token { kind: ')', .. })) {
return Ok(None);
}
self.toks.next();
ident.push_str(&value);
ident.push(')');
Ok(Some(ident))
}
pub(crate) fn declaration_value(
&mut self,
allow_empty: bool,
allow_semicolon: bool,
allow_colon: bool,
) -> SassResult<String> {
let mut buffer = String::new();
let mut brackets = Vec::new();
let mut wrote_newline = false;
while let Some(tok) = self.toks.peek() {
match tok.kind {
'\\' => {
self.toks.next();
buffer.push_str(&self.parse_escape(true)?);
wrote_newline = false;
}
q @ ('"' | '\'') => {
self.toks.next();
let s = self.parse_quoted_string(q)?;
buffer.push_str(&s.node.to_css_string(s.span, self.options.is_compressed())?);
wrote_newline = false;
}
'/' => {
if matches!(self.toks.peek_n(1), Some(Token { kind: '*', .. })) {
self.toks.next();
let comment = match self.parse_comment()?.node {
Comment::Loud(s) => s,
Comment::Silent => continue,
};
buffer.push_str("/*");
buffer.push_str(&comment);
buffer.push_str("*/");
} else {
buffer.push('/');
self.toks.next();
}
wrote_newline = false;
}
'#' => {
if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) {
let s = self.parse_identifier()?;
buffer.push_str(&s.node);
} else {
buffer.push('#');
self.toks.next();
}
wrote_newline = false;
}
c @ (' ' | '\t') => {
if wrote_newline
|| !self.toks.peek_n(1).map_or(false, |tok| tok.is_whitespace())
{
buffer.push(c);
}
self.toks.next();
}
'\n' | '\r' => {
if !wrote_newline {
buffer.push('\n');
}
wrote_newline = true;
self.toks.next();
}
'[' | '(' | '{' => {
buffer.push(tok.kind);
self.toks.next();
match tok.kind {
'[' => brackets.push(']'),
'(' => brackets.push(')'),
'{' => brackets.push('}'),
_ => unreachable!(),
}
wrote_newline = false;
}
']' | ')' | '}' => {
if let Some(end) = brackets.pop() {
self.expect_char(end)?;
} else {
break;
}
wrote_newline = false;
}
';' => {
if !allow_semicolon && brackets.is_empty() {
break;
}
self.toks.next();
buffer.push(';');
wrote_newline = false;
}
':' => {
if !allow_colon && brackets.is_empty() {
break;
}
self.toks.next();
buffer.push(':');
wrote_newline = false;
}
'u' | 'U' => {
let before_url = self.toks.cursor();
if !self.scan_identifier("url", true) {
buffer.push(tok.kind);
self.toks.next();
wrote_newline = false;
continue;
}
if let Some(contents) = self.try_parse_url()? {
buffer.push_str(&contents);
} else {
self.toks.set_cursor(before_url);
buffer.push(tok.kind);
self.toks.next();
}
wrote_newline = false;
}
c => {
if self.looking_at_identifier() {
buffer.push_str(&self.parse_identifier()?.node);
} else {
self.toks.next();
buffer.push(c);
}
wrote_newline = false;
}
}
}
if let Some(last) = brackets.pop() {
self.expect_char(last)?;
}
if !allow_empty && buffer.is_empty() {
return Err(("Expected token.", self.span_before).into());
}
Ok(buffer)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
pub(crate) use eval::{HigherIntermediateValue, ValueVisitor};
mod css_function;
mod eval;
mod parse;

File diff suppressed because it is too large Load Diff

View File

@ -1,162 +0,0 @@
use codemap::Spanned;
use crate::{common::Identifier, error::SassResult, value::Value, Token};
use super::Parser;
#[derive(Debug)]
pub(crate) struct VariableValue {
pub var_value: SassResult<Spanned<Value>>,
pub global: bool,
pub default: bool,
}
impl VariableValue {
pub const fn new(var_value: SassResult<Spanned<Value>>, global: bool, default: bool) -> Self {
Self {
var_value,
global,
default,
}
}
}
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn parse_variable_declaration(&mut self) -> SassResult<()> {
let next = self.toks.next();
assert!(matches!(next, Some(Token { kind: '$', .. })));
let ident: Identifier = self.parse_identifier_no_interpolation(false)?.node.into();
self.whitespace_or_comment();
self.expect_char(':')?;
let VariableValue {
var_value,
global,
default,
} = self.parse_variable_value()?;
if default {
let config_val = self.module_config.get(ident).filter(|v| !v.is_null());
let value = if (self.at_root && !self.flags.in_control_flow()) || global {
if self.global_scope.default_var_exists(ident) {
return Ok(());
} else if let Some(value) = config_val {
value
} else {
var_value?.node
}
} else if self.at_root && self.flags.in_control_flow() {
if self.global_scope.default_var_exists(ident) {
return Ok(());
}
var_value?.node
} else {
if self.scopes.default_var_exists(ident) {
return Ok(());
}
var_value?.node
};
if self.at_root && self.global_scope.var_exists(ident) {
if !self.global_scope.default_var_exists(ident) {
self.global_scope.insert_var(ident, value.clone());
}
} else if self.at_root
&& !self.flags.in_control_flow()
&& !self.global_scope.default_var_exists(ident)
{
self.global_scope.insert_var(ident, value.clone());
}
if global {
self.global_scope.insert_var(ident, value.clone());
}
if self.at_root && !self.flags.in_control_flow() {
return Ok(());
}
self.scopes.insert_var(ident, value);
return Ok(());
}
let value = var_value?.node;
if global {
self.global_scope.insert_var(ident, value.clone());
}
if self.at_root {
if self.flags.in_control_flow() {
if self.global_scope.var_exists(ident) {
self.global_scope.insert_var(ident, value);
} else {
self.scopes.insert_var(ident, value);
}
} else {
self.global_scope.insert_var(ident, value);
}
} else if !(self.flags.in_control_flow() && global) {
self.scopes.insert_var(ident, value);
}
Ok(())
}
pub(super) fn parse_variable_value(&mut self) -> SassResult<VariableValue> {
let mut default = false;
let mut global = false;
let value = self.parse_value(true, &|parser| {
if matches!(parser.toks.peek(), Some(Token { kind: '!', .. })) {
let is_important = matches!(
parser.toks.peek_next(),
Some(Token { kind: 'i', .. })
| Some(Token { kind: 'I', .. })
| Some(Token { kind: '=', .. })
);
parser.toks.reset_cursor();
!is_important
} else {
false
}
});
// todo: it should not be possible to declare the same flag more than once
while self.consume_char_if_exists('!') {
let flag = self.parse_identifier_no_interpolation(false)?;
match flag.node.as_str() {
"global" => {
global = true;
}
"default" => {
default = true;
}
_ => {
return Err(("Invalid flag name.", flag.span).into());
}
}
self.whitespace_or_comment();
}
match self.toks.peek() {
Some(Token { kind: ';', .. }) => {
self.toks.next();
}
Some(Token { kind: '}', .. }) => {}
Some(..) | None => {
value?;
self.expect_char(';')?;
unreachable!();
}
}
Ok(VariableValue::new(value, global, default))
}
}

View File

@ -1,268 +0,0 @@
use std::collections::BTreeMap;
use codemap::Spanned;
use crate::{
atrule::mixin::Mixin,
builtin::GLOBAL_FUNCTIONS,
common::Identifier,
error::SassResult,
value::{SassFunction, Value},
};
/// A singular scope
///
/// Contains variables, functions, and mixins
#[derive(Debug, Default, Clone)]
pub(crate) struct Scope {
pub vars: BTreeMap<Identifier, Value>,
pub mixins: BTreeMap<Identifier, Mixin>,
pub functions: BTreeMap<Identifier, SassFunction>,
}
impl Scope {
// `BTreeMap::new` is not yet const
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn new() -> Self {
Self {
vars: BTreeMap::new(),
mixins: BTreeMap::new(),
functions: BTreeMap::new(),
}
}
fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
match self.vars.get(&name.node) {
Some(v) => Ok(v),
None => Err(("Undefined variable.", name.span).into()),
}
}
fn get_var_no_err(&self, name: Identifier) -> Option<&Value> {
self.vars.get(&name)
}
pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
self.vars.insert(s, v)
}
pub fn var_exists(&self, name: Identifier) -> bool {
self.vars.contains_key(&name)
}
fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
match self.mixins.get(&name.node) {
Some(v) => Ok(v.clone()),
None => Err(("Undefined mixin.", name.span).into()),
}
}
pub fn insert_mixin<T: Into<Identifier>>(&mut self, s: T, v: Mixin) -> Option<Mixin> {
self.mixins.insert(s.into(), v)
}
pub fn mixin_exists(&self, name: Identifier) -> bool {
self.mixins.contains_key(&name)
}
fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
self.functions.get(&name).cloned()
}
pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option<SassFunction> {
self.functions.insert(s, v)
}
pub fn fn_exists(&self, name: Identifier) -> bool {
if self.functions.is_empty() {
return false;
}
self.functions.contains_key(&name)
}
fn merge(&mut self, other: Scope) {
self.vars.extend(other.vars);
self.mixins.extend(other.mixins);
self.functions.extend(other.functions);
}
pub fn merge_module_scope(&mut self, other: Scope) {
self.merge(other);
}
pub fn default_var_exists(&self, s: Identifier) -> bool {
if let Some(default_var) = self.get_var_no_err(s) {
!default_var.is_null()
} else {
false
}
}
}
#[derive(Debug, Default)]
pub(crate) struct Scopes(Vec<Scope>);
impl Scopes {
pub const fn new() -> Self {
Self(Vec::new())
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn split_off(mut self, len: usize) -> (Scopes, Scopes) {
let split = self.0.split_off(len);
(self, Scopes(split))
}
pub fn enter_new_scope(&mut self) {
self.0.push(Scope::new());
}
pub fn enter_scope(&mut self, scope: Scope) {
self.0.push(scope);
}
pub fn exit_scope(&mut self) {
self.0.pop();
}
pub fn merge(&mut self, mut other: Self) {
self.0.append(&mut other.0);
}
}
/// Variables
impl Scopes {
pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
for scope in self.0.iter_mut().rev() {
if scope.var_exists(s) {
return scope.insert_var(s, v);
}
}
if let Some(scope) = self.0.last_mut() {
scope.insert_var(s, v)
} else {
let mut scope = Scope::new();
scope.insert_var(s, v);
self.0.push(scope);
None
}
}
/// Always insert this variable into the innermost scope
///
/// Used, for example, for variables from `@each` and `@for`
pub fn insert_var_last(&mut self, s: Identifier, v: Value) -> Option<Value> {
if let Some(scope) = self.0.last_mut() {
scope.insert_var(s, v)
} else {
let mut scope = Scope::new();
scope.insert_var(s, v);
self.0.push(scope);
None
}
}
pub fn default_var_exists(&self, name: Identifier) -> bool {
for scope in self.0.iter().rev() {
if scope.default_var_exists(name) {
return true;
}
}
false
}
pub fn get_var<'a>(
&'a self,
name: Spanned<Identifier>,
global_scope: &'a Scope,
) -> SassResult<&Value> {
for scope in self.0.iter().rev() {
if scope.var_exists(name.node) {
return scope.get_var(name);
}
}
global_scope.get_var(name)
}
pub fn var_exists(&self, name: Identifier, global_scope: &Scope) -> bool {
for scope in &self.0 {
if scope.var_exists(name) {
return true;
}
}
global_scope.var_exists(name)
}
}
/// Mixins
impl Scopes {
pub fn insert_mixin(&mut self, s: Identifier, v: Mixin) -> Option<Mixin> {
if let Some(scope) = self.0.last_mut() {
scope.insert_mixin(s, v)
} else {
let mut scope = Scope::new();
scope.insert_mixin(s, v);
self.0.push(scope);
None
}
}
pub fn get_mixin<'a>(
&'a self,
name: Spanned<Identifier>,
global_scope: &'a Scope,
) -> SassResult<Mixin> {
for scope in self.0.iter().rev() {
if scope.mixin_exists(name.node) {
return scope.get_mixin(name);
}
}
global_scope.get_mixin(name)
}
pub fn mixin_exists(&self, name: Identifier, global_scope: &Scope) -> bool {
for scope in &self.0 {
if scope.mixin_exists(name) {
return true;
}
}
global_scope.mixin_exists(name)
}
}
/// Functions
impl Scopes {
pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option<SassFunction> {
if let Some(scope) = self.0.last_mut() {
scope.insert_fn(s, v)
} else {
let mut scope = Scope::new();
scope.insert_fn(s, v);
self.0.push(scope);
None
}
}
pub fn get_fn<'a>(&'a self, name: Identifier, global_scope: &'a Scope) -> Option<SassFunction> {
for scope in self.0.iter().rev() {
if scope.fn_exists(name) {
return scope.get_fn(name);
}
}
global_scope.get_fn(name)
}
pub fn fn_exists(&self, name: Identifier, global_scope: &Scope) -> bool {
for scope in &self.0 {
if scope.fn_exists(name) {
return true;
}
}
global_scope.fn_exists(name) || GLOBAL_FUNCTIONS.contains_key(name.as_str())
}
}

Some files were not shown because too many files have changed in this diff Show More