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

View File

@ -73,10 +73,10 @@ jobs:
- name: Build
run: cargo build
- name: Install dart-sass 1.36.0
- name: Install dart-sass 1.54.3
run: |
wget https://github.com/sass/dart-sass/releases/download/1.36.0/dart-sass-1.36.0-linux-x64.tar.gz
tar -xzvf 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.54.3-linux-x64.tar.gz
- name: Install bootstrap
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
- make `grass::Error` a `Send` type

View File

@ -23,63 +23,32 @@ path = "src/lib.rs"
# crate-type = ["cdylib", "rlib"]
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]
clap = { version = "2.34.0", optional = true }
num-rational = "0.4"
num-bigint = "0.4"
num-traits = "0.2.14"
# todo: use lazy_static
once_cell = "1.15.0"
# todo: use xorshift for random numbers
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"
wasm-bindgen = { version = "0.2.68", optional = true }
beef = "0.5"
# todo: use phf for global functions
phf = { version = "0.11", features = ["macros"] }
# criterion is not a dev-dependency because it makes tests take too
# long to compile, and you cannot make dev-dependencies optional
criterion = { version = "0.4.0", optional = true }
indexmap = "1.9.1"
indexmap = "1.9.0"
# todo: do we really need interning for things?
lasso = "0.6"
[features]
# todo: no commandline by default
default = ["commandline", "random"]
# Option (enabled by default): build a binary using 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()`
random = ["rand"]
# Option: expose JavaScript-friendly WebAssembly exports
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]
tempfile = "3.3.0"

View File

@ -3,17 +3,12 @@
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.
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.
This crate aims to achieve complete feature parity with the `dart-sass` reference
implementation. A deviation from the `dart-sass` implementation can be considered
a bug except for in the following situations:
- Error messages
- Error spans
- Certain aspects of the indented syntax
- Potentially others in the future
a bug except for in the case of error message and error spans.
[Documentation](https://docs.rs/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.
That said, there are a number of known missing features and bugs. The notable features remaining are
```
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`
```
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.
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
[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
`npm install @connorskees/grass` to add it to your package.json. Better documentation
for this version will be provided when the library becomes more stable.
compiled using wasm-bindgen. To use `grass` in your JavaScript projects, run
`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).
## Features
## Cargo Features
### commandline
@ -73,28 +57,16 @@ are in the official spec.
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
# This script expects node >=v14.14.0. Check version with `node --version`
git clone https://github.com/connorskees/grass --recursive
cd grass && cargo b --release
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.
```
@ -103,3 +75,5 @@ PASSING: 4205
FAILING: 2051
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 {
background: red;
}
a {
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;
use std::fmt::{self, Write};
use crate::{parse::Stmt, selector::Selector};
use codemap::Span;
use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser, token::Token};
#[derive(Debug, Clone)]
pub(crate) struct MediaRule {
pub super_selector: Selector,
pub query: String,
pub body: Vec<Stmt>,
pub query: Vec<MediaQuery>,
pub body: Vec<CssStmt>,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
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>,
/// 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>,
/// Feature queries, including parentheses.
pub features: Vec<String>,
pub conditions: Vec<String>,
pub conjunction: bool,
}
impl MediaQuery {
pub fn is_condition(&self) -> bool {
self.modifier.is_none() && self.media_type.is_none()
}
pub fn matches_all_types(&self) -> bool {
self.media_type.is_none()
|| self
@ -39,16 +27,44 @@ impl MediaQuery {
.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 {
modifier: 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)]
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_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());
@ -56,33 +72,34 @@ impl MediaQuery {
if this_type.is_none() && other_type.is_none() {
return MediaQueryMergeResult::Success(Self::condition(
self.features
self.conditions
.iter()
.chain(&other.features)
.chain(&other.conditions)
.cloned()
.collect(),
true,
));
}
let modifier;
let media_type;
let features;
let conditions;
if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) {
if this_modifier == other_modifier {
let negative_features = if this_modifier.as_deref() == Some("not") {
&self.features
let negative_conditions = if this_modifier.as_deref() == Some("not") {
&self.conditions
} else {
&other.features
&other.conditions
};
let positive_features = if this_modifier.as_deref() == Some("not") {
&other.features
let positive_conditions = if this_modifier.as_deref() == Some("not") {
&other.conditions
} 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
// 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
// a screen with no color but with a grid.
if negative_features
if negative_conditions
.iter()
.all(|feat| positive_features.contains(feat))
.all(|feat| positive_conditions.contains(feat))
{
return MediaQueryMergeResult::Empty;
}
@ -105,11 +122,11 @@ impl MediaQuery {
if this_modifier.as_deref() == Some("not") {
modifier = &other_modifier;
media_type = &other_type;
features = other.features.clone();
conditions = other.conditions.clone();
} else {
modifier = &this_modifier;
media_type = &this_type;
features = self.features.clone();
conditions = self.conditions.clone();
}
} else if this_modifier.as_deref() == Some("not") {
debug_assert_eq!(other_modifier.as_deref(), Some("not"));
@ -119,27 +136,27 @@ impl MediaQuery {
return MediaQueryMergeResult::Unrepresentable;
}
let more_features = if self.features.len() > other.features.len() {
&self.features
let more_conditions = if self.conditions.len() > other.conditions.len() {
&self.conditions
} else {
&other.features
&other.conditions
};
let fewer_features = if self.features.len() > other.features.len() {
&other.features
let fewer_conditions = if self.conditions.len() > other.conditions.len() {
&other.conditions
} 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.
if fewer_features
if fewer_conditions
.iter()
.all(|feat| more_features.contains(feat))
.all(|feat| more_conditions.contains(feat))
{
modifier = &this_modifier; // "not"
modifier = &this_modifier;
media_type = &this_type;
features = more_features.clone();
conditions = more_conditions.clone();
} else {
// Otherwise, there's no way to represent the intersection.
return MediaQueryMergeResult::Unrepresentable;
@ -155,19 +172,19 @@ impl MediaQuery {
&other_type
};
features = self
.features
conditions = self
.conditions
.iter()
.chain(&other.features)
.chain(&other.conditions)
.cloned()
.collect();
} else if other.matches_all_types() {
modifier = &this_modifier;
media_type = &this_type;
features = self
.features
conditions = self
.conditions
.iter()
.chain(&other.features)
.chain(&other.conditions)
.cloned()
.collect();
} else if this_type != other_type {
@ -180,10 +197,10 @@ impl MediaQuery {
}
media_type = &this_type;
features = self
.features
conditions = self
.conditions
.iter()
.chain(&other.features)
.chain(&other.conditions)
.cloned()
.collect();
}
@ -199,7 +216,8 @@ impl MediaQuery {
} else {
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 {
if let Some(modifier) = &self.modifier {
f.write_str(modifier)?;
f.write_char(' ')?;
}
if let Some(media_type) = &self.media_type {
f.write_str(media_type)?;
if !&self.features.is_empty() {
if !&self.conditions.is_empty() {
f.write_str(" and ")?;
}
}
f.write_str(&self.features.join(" and "))
f.write_str(&self.conditions.join(" and "))
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
enum MediaQueryMergeResult {
pub(crate) enum MediaQueryMergeResult {
Empty,
Unrepresentable,
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)]
#[allow(dead_code)]
pub(crate) struct UnknownAtRule {
pub name: String,
pub super_selector: Selector,
// pub super_selector: Selector,
pub params: String,
pub body: Vec<Stmt>,
pub body: Vec<CssStmt>,
/// Whether or not this @-rule was declared with curly
/// 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,219 +1,136 @@
use super::{Builtin, GlobalFunctionMap};
use std::collections::{BTreeMap, BTreeSet};
use codemap::Spanned;
use num_traits::One;
use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber};
use crate::{
args::CallArgs,
color::Color,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
use super::rgb::{function_string, parse_channels, percentage_or_unitless, ParsedChannels};
fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
if args.is_empty() {
return Err(("Missing argument $channels.", args.span()).into());
fn hsl_3_args(
name: &'static str,
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
let span = args.span();
let hue = args.get_err(0, "hue")?;
let saturation = args.get_err(1, "saturation")?;
let lightness = args.get_err(2, "lightness")?;
let alpha = args.default_arg(
3,
"alpha",
Value::Dimension(SassNumber {
num: (Number::one()),
unit: Unit::None,
as_slash: None,
}),
);
if [&hue, &saturation, &lightness, &alpha]
.iter()
.copied()
.any(Value::is_special_function)
{
return Ok(Value::String(
format!(
"{}({})",
name,
Value::List(
if args.len() == 4 {
vec![hue, saturation, lightness, alpha]
} else {
vec![hue, saturation, lightness]
},
ListSeparator::Comma,
Brackets::None
)
.to_css_string(args.span(), false)?
),
QuoteKind::None,
));
}
let hue = hue.assert_number_with_name("hue", span)?;
let saturation = saturation.assert_number_with_name("saturation", span)?;
let lightness = lightness.assert_number_with_name("lightness", span)?;
let alpha = percentage_or_unitless(
&alpha.assert_number_with_name("alpha", span)?,
1.0,
"alpha",
span,
visitor,
)?;
Ok(Value::Color(Box::new(Color::from_hsla_fn(
Number(hue.num().rem_euclid(360.0)),
saturation.num() / Number::from(100),
lightness.num() / Number::from(100),
Number(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 {
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 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(),
};
if channels.len() > 3 {
return Err((
format!(
"Only 3 elements allowed, but {} were passed.",
channels.len()
),
args.span(),
)
.into());
hsl_3_args(name, args, visitor)
}
}
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 {
} else if len == 2 {
let hue = args.get_err(0, "hue")?;
let saturation = args.get_err(1, "saturation")?;
let lightness = args.get_err(2, "lightness")?;
let alpha = args.default_arg(
3,
"alpha",
Value::Dimension(Some(Number::one()), Unit::None, true),
)?;
if [&hue, &saturation, &lightness, &alpha]
.iter()
.copied()
.any(Value::is_special_function)
{
if hue.is_var() || saturation.is_var() {
return Ok(Value::String(
format!(
"{}({})",
name,
Value::List(
if len == 4 {
vec![hue, saturation, lightness, alpha]
} else {
vec![hue, saturation, lightness]
},
ListSeparator::Comma,
Brackets::None
)
.to_css_string(args.span(), false)?
),
function_string(name, &[hue, saturation], visitor, span)?,
QuoteKind::None,
));
} else {
return Err(("Missing argument $lightness.", args.span()).into());
}
let hue = match hue {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let saturation = match saturation {
Value::Dimension(Some(n), ..) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
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,
))))
} else {
return hsl_3_args(name, args, visitor);
}
}
pub(crate) fn hsl(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
inner_hsl("hsl", args, parser)
pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
inner_hsl("hsl", args, visitor)
}
pub(crate) fn hsla(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
inner_hsl("hsla", args, parser)
pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
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)?;
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((
format!("$color: {} is not a color.", v.inspect(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)?;
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((
format!("$color: {} is not a color.", v.inspect(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)?;
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((
format!("$color: {} is not a color.", v.inspect(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)?;
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 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())
}
};
let color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
let degrees = args
.get_err(1, "degrees")?
.assert_number_with_name("degrees", args.span())?
.num();
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)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
@ -287,24 +196,16 @@ fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
.into())
}
};
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(None, ..) => todo!(),
v => {
return Err((
format!(
"$amount: {} is not a number.",
v.to_css_string(args.span(), false)?
),
args.span(),
)
.into())
}
};
let amount = args
.get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?;
let amount = bound!(args, "amount", amount.num(), amount.unit, 0, 100) / Number(100.0);
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)?;
let color = match args.get_err(0, "color")? {
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")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
Value::Dimension(None, ..) => todo!(),
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
v => {
return Err((
format!(
@ -333,22 +238,29 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
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)?;
if args.len() == 1 {
let amount = args
.get_err(0, "amount")?
.assert_number_with_name("amount", args.span())?;
return Ok(Value::String(
format!(
"saturate({})",
args.get_err(0, "amount")?
.to_css_string(args.span(), false)?
serialize_number(&amount, &Options::default(), args.span())?,
),
QuoteKind::None,
));
}
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(None, ..) => todo!(),
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
v => {
return Err((
format!(
@ -362,11 +274,16 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
};
let color = match args.get_err(0, "color")? {
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(
format!("saturate({}{})", n.inspect(), u),
QuoteKind::None,
))
));
}
v => {
return Err((
@ -379,7 +296,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
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)?;
let color = match args.get_err(0, "color")? {
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")? {
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
Value::Dimension(None, ..) => todo!(),
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
v => {
return Err((
format!(
"$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(),
)
@ -408,11 +329,15 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
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)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
Value::Dimension(Some(n), u, _) => {
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => {
return Ok(Value::String(
format!("grayscale({}{})", n.inspect(), u),
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()))))
}
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)?;
let color = match args.get_err(0, "color")? {
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())))
}
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)?;
let weight = match args.get(1, "weight") {
Some(Err(e)) => return Err(e),
Some(Ok(Spanned {
node: Value::Dimension(Some(n), u, _),
Some(Spanned {
node: Value::Dimension(SassNumber { num: n, .. }),
..
})) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
Some(Ok(Spanned {
node: Value::Dimension(None, ..),
}) if n.is_nan() => todo!(),
Some(Spanned {
node:
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}),
..
})) => todo!(),
}) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
None => None,
Some(Ok(v)) => {
Some(v) => {
return Err((
format!(
"$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(),
)
@ -472,7 +401,11 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
Value::Color(c) => Ok(Value::Color(Box::new(
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() {
return Err((
"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,
))
}
Value::Dimension(None, u, _) => {
Ok(Value::String(format!("invert(NaN{})", u), QuoteKind::None))
}
v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),

View File

@ -1,15 +1,8 @@
use num_traits::One;
use crate::builtin::builtin_imports::*;
use crate::{
args::CallArgs,
color::Color,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
use super::rgb::{parse_channels, ParsedChannels};
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)?;
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 =
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)?;
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 color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
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> {
args.max_args(4)?;
if args.is_empty() {
return Err(("Missing argument $channels.", args.span()).into());
}
fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span();
let hue = match args.get(0, "hue") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
Some(v) => match v.node {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { num: n, .. }) => n,
v => {
return Err((
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())
}
},
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $hue.", args.span()).into()),
};
let whiteness = match args.get(1, "whiteness") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), Unit::Percent, ..) => n,
v @ Value::Dimension(Some(..), ..) => {
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 whiteness = args
.get_err(1, "whiteness")?
.assert_number_with_name("whiteness", span)?;
whiteness.assert_unit(&Unit::Percent, "whiteness", span)?;
let blackness = match args.get(2, "blackness") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
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 blackness = args
.get_err(2, "blackness")?
.assert_number_with_name("blackness", span)?;
blackness.assert_unit(&Unit::Percent, "blackness", span)?;
let alpha = match args.get(3, "alpha") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), Unit::Percent, ..) => n / Number::from(100),
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
Some(v) => match v.node {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: Unit::Percent,
..
}) => n / Number::from(100),
Value::Dimension(SassNumber { num: n, .. }) => n,
v => {
return Err((
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())
}
},
Some(Err(e)) => return Err(e),
None => Number::one(),
};
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 hwb;

View File

@ -1,9 +1,4 @@
use super::{Builtin, GlobalFunctionMap};
use crate::{
args::CallArgs, common::QuoteKind, error::SassResult, parse::Parser, unit::Unit, value::Number,
value::Value,
};
use crate::builtin::builtin_imports::*;
/// Check if `s` matches the regex `^[a-zA-Z]+\s*=`
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 {
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) => {
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)?;
match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)),
Value::Dimension(Some(num), unit, _) => Ok(Value::String(
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
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),
QuoteKind::None,
)),
Value::Dimension(None, ..) => todo!(),
v => Err((
format!("$color: {} is not a color.", v.inspect(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: CallArgs, parser: &mut Parser) -> SassResult<Value> {
fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
@ -99,21 +105,16 @@ fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
.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())
}
};
let amount = args
.get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?;
let amount = bound!(args, "amount", amount.num(), amount.unit(), 0, 1);
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)?;
let color = match args.get_err(0, "color")? {
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")? {
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_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!(),
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "amount", n, u, 0, 1),
v => {
return Err((
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("opacity", Builtin::new(opacity));
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("fade-out", Builtin::new(fade_out));
f.insert("fade-out", Builtin::new(transparentize));
}

View File

@ -1,22 +1,12 @@
use super::{Builtin, GlobalFunctionMap};
use num_traits::{One, Signed, Zero};
use crate::{
args::CallArgs,
color::Color,
common::QuoteKind,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
use crate::builtin::builtin_imports::*;
macro_rules! opt_rgba {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
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(None, ..) => todo!(),
let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => Some(bound!($args, $arg, n, u, $low, $high)),
Value::Null => None,
v => {
return Err((
@ -31,11 +21,11 @@ macro_rules! opt_rgba {
macro_rules! opt_hsl {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? {
Value::Dimension(Some(n), u, _) => {
Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100))
}
Value::Dimension(None, ..) => todo!(),
let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100)),
Value::Null => None,
v => {
return Err((
@ -48,8 +38,8 @@ macro_rules! opt_hsl {
};
}
pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
if args.positional_arg(1).is_some() {
pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
if args.get_positional(1).is_some() {
return Err((
"Only one positional argument is allowed. All other arguments must be passed by name.",
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)? {
Value::Dimension(Some(n), ..) => Some(n),
Value::Dimension(None, ..) => todo!(),
let hue = match args.default_named_arg("hue", Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
Value::Null => None,
v => {
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")? {
Value::Color(c) => c,
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)? {
Value::Dimension(Some(n), ..) => Some(n),
Value::Dimension(None, ..) => todo!(),
let hue = match args.default_named_arg("hue", Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
Value::Null => None,
v => {
return Err((
@ -179,12 +169,12 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
#[allow(clippy::cognitive_complexity)]
// 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 {
if by.is_zero() {
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();
@ -201,12 +191,14 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
macro_rules! opt_scale_arg {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? {
Value::Dimension(Some(n), Unit::Percent, _) => {
Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100))
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: Unit::Percent,
..
}) => Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)),
v @ Value::Dimension { .. } => {
return Err((
format!(
"${}: 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)?;
let color = match args.get_err(0, "color")? {
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::{
args::CallArgs,
color::Color,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
Ok(format!("{}({})", name, args))
}
/// 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());
fn inner_rgb_2_arg(
name: &'static str,
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
// and functions are parsed after variable substitution.
let color = args.get_err(0, "color")?;
let alpha = args.get_err(1, "alpha")?;
let is_compressed = visitor.options.is_compressed();
if color.is_var() {
return Ok(Value::String(
function_string(name, &[color, alpha], visitor, args.span())?,
QuoteKind::None,
));
} else if alpha.is_var() {
match &color {
Value::Color(color) => {
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,
));
}
_ => {
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,
));
}
let len = args.len();
let color = color.assert_color_with_name("color", args.span())?;
let alpha = alpha.assert_number_with_name("alpha", args.span())?;
Ok(Value::Color(Box::new(color.with_alpha(Number(
percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?,
)))))
}
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()),
fn inner_rgb_3_arg(
name: &'static str,
mut args: ArgumentResult,
visitor: &mut Visitor,
) -> SassResult<Value> {
let alpha = if args.len() > 3 {
args.get(3, "alpha")
} else {
None
};
let red = args.get_err(0, "red")?;
let green = args.get_err(1, "green")?;
let blue = args.get_err(2, "blue")?;
if red.is_special_function()
|| green.is_special_function()
|| blue.is_special_function()
|| alpha
.as_ref()
.map(|alpha| alpha.node.is_special_function())
.unwrap_or(false)
{
let fn_string = if alpha.is_some() {
function_string(
name,
&[red, green, blue, alpha.unwrap().node],
visitor,
args.span(),
)?
} else {
function_string(name, &[red, green, blue], visitor, args.span())?
};
if channels.len() > 3 {
return Ok(Value::String(fn_string, QuoteKind::None));
}
let span = args.span();
let red = red.assert_number_with_name("red", span)?;
let green = green.assert_number_with_name("green", span)?;
let blue = blue.assert_number_with_name("blue", span)?;
Ok(Value::Color(Box::new(Color::from_rgba_fn(
Number(fuzzy_round(percentage_or_unitless(
&red, 255.0, "red", span, visitor,
)?)),
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),
),
))))
}
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)
}
#[derive(Debug, Clone)]
pub(crate) enum ParsedChannels {
String(String),
List(Vec<Value>),
}
fn is_var_slash(value: &Value) -> bool {
match value {
Value::String(text, QuoteKind::Quoted) => {
text.to_ascii_lowercase().starts_with("var(") && text.contains('/')
}
_ => false,
}
}
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 3 elements allowed, but {} were passed.",
channels.len()
"Only 2 slash-separated elements allowed, but {} {} passed.",
list.len(),
if list.len() == 1 { "was" } else { "were" }
),
args.span(),
span,
)
.into());
}
if channels.iter().any(Value::is_special_function) {
let channel_sep = if channels.len() < 3 {
ListSeparator::Space
} else {
ListSeparator::Comma
};
channels = list[0].clone();
let inner_alpha_from_slash_list = list[1].clone();
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 alpha = args.get_err(1, "alpha")?;
if color.is_special_function() || (alpha.is_special_function() && !color.is_color()) {
return Ok(Value::String(
format!(
"{}({})",
name,
Value::List(vec![color, alpha], ListSeparator::Comma, Brackets::None)
.to_css_string(args.span(), false)?
),
QuoteKind::None,
));
}
let color = match 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(
format!(
"{}({}, {}, {}, {})",
name,
color.red().to_string(false),
color.green().to_string(false),
color.blue().to_string(false),
alpha.to_css_string(args.span(), false)?,
),
QuoteKind::None,
));
}
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.with_alpha(alpha))))
} else {
let red = args.get_err(0, "red")?;
let green = args.get_err(1, "green")?;
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]
.iter()
.copied()
.any(Value::is_special_function)
if !alpha_from_slash_list
.as_ref()
.map(Value::is_special_function)
.unwrap_or(false)
{
return Ok(Value::String(
format!(
"{}({})",
name,
Value::List(
if len == 4 {
vec![red, green, blue, alpha]
} else {
vec![red, green, blue]
},
ListSeparator::Comma,
Brackets::None
)
.to_css_string(args.span(), false)?
),
QuoteKind::None,
));
inner_alpha_from_slash_list
.clone()
.assert_number_with_name("alpha", span)?;
}
let red = match red {
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
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");
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
"$red: Expected {} to have no units or \"%\".",
v.to_css_string(args.span(), parser.options.is_compressed())?
),
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,
))))
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)),
}
}
pub(crate) fn rgb(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
inner_rgb("rgb", args, parser)
/// 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 rgba(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
inner_rgb("rgba", args, parser)
pub(crate) fn rgb(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
inner_rgb("rgb", args, visitor)
}
pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
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)?;
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((
format!("$color: {} is not a color.", v.inspect(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)?;
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((
format!("$color: {} is not a color.", v.inspect(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)?;
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((
format!("$color: {} is not a color.", v.inspect(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)?;
let color1 = match args.get_err(0, "color1")? {
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(
2,
"weight",
Value::Dimension(Some(Number::from(50)), Unit::None, true),
)? {
Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
Value::Dimension(None, ..) => todo!(),
Value::Dimension(SassNumber {
num: (Number::from(50)),
unit: Unit::None,
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 => {
return Err((
format!(
"$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(),
)

View File

@ -1,33 +1,24 @@
use super::{Builtin, GlobalFunctionMap};
use crate::builtin::builtin_imports::*;
use num_traits::{Signed, ToPrimitive, Zero};
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> {
pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
Ok(Value::Dimension(
Some(Number::from(args.get_err(0, "list")?.as_list().len())),
Unit::None,
true,
))
Ok(Value::Dimension(SassNumber {
num: (Number::from(args.get_err(0, "list")?.as_list().len())),
unit: Unit::None,
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)?;
let mut list = args.get_err(0, "list")?.as_list();
let (n, unit) = match args.get_err(1, "n")? {
Value::Dimension(Some(num), unit, ..) => (num, unit),
Value::Dimension(None, u, ..) => {
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) if n.is_nan() => {
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
}
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
v => {
return Err((
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());
}
if n.is_decimal() {
return Err((format!("$n: {} is not an int.", n.inspect()), args.span()).into());
}
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 {
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)?;
Ok(Value::String(
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)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
Value::List(v, sep, b) => (v, sep, b),
Value::ArgList(v) => (
v.into_iter().map(|val| val.node).collect(),
v.elems.into_iter().collect(),
ListSeparator::Comma,
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),
};
let (n, unit) = match args.get_err(1, "n")? {
Value::Dimension(Some(num), unit, ..) => (num, unit),
Value::Dimension(None, u, ..) => {
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) if n.is_nan() => {
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
}
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
v => {
return Err((
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")?;
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 {
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))
}
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)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
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,
"separator",
Value::String("auto".to_owned(), QuoteKind::None),
)? {
) {
Value::String(s, ..) => match s.as_str() {
"auto" => sep,
"comma" => ListSeparator::Comma,
"space" => ListSeparator::Space,
"slash" => ListSeparator::Slash,
_ => {
return Err((
"$separator: Must be \"space\", \"comma\", or \"auto\".",
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
args.span(),
)
.into())
@ -176,7 +168,7 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
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)?;
let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
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,
"separator",
Value::String("auto".to_owned(), QuoteKind::None),
)? {
) {
Value::String(s, ..) => match s.as_str() {
"auto" => {
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,
"space" => ListSeparator::Space,
"slash" => ListSeparator::Slash,
_ => {
return Err((
"$separator: Must be \"space\", \"comma\", or \"auto\".",
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
args.span(),
)
.into())
@ -224,7 +217,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
3,
"bracketed",
Value::String("auto".to_owned(), QuoteKind::None),
)? {
) {
Value::String(s, ..) => match s.as_str() {
"auto" => brackets,
_ => Brackets::Bracketed,
@ -243,7 +236,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
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)?;
Ok(Value::bool(match args.get_err(0, "list")? {
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)?;
let list = args.get_err(0, "list")?.as_list();
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),
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
.get_variadic()?
.into_iter()

View File

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

View File

@ -1,14 +1,6 @@
use super::{Builtin, GlobalFunctionMap};
use crate::builtin::builtin_imports::*;
use crate::{
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> {
pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?;
let key = args.get_err(1, "key")?;
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))
}
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)?;
let key = args.get_err(1, "key")?;
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()))
}
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)?;
let map = match args.get_err(0, "map")? {
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)?;
let map = match args.get_err(0, "map")? {
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 {
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() {
match map_queue.last_mut() {
Some((_, map)) => {
map.insert(key.node, Value::Map(queued_map));
map.insert(key, Value::Map(queued_map));
}
None => {
map1.insert(key.node, Value::Map(queued_map));
map1.insert(key, Value::Map(queued_map));
break;
}
}
@ -163,7 +155,7 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
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")? {
Value::Map(m) => m,
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))
}
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 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 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() {
match map_queue.last_mut() {
Some((_, next_map)) => {
next_map.insert(key.node, Value::Map(queued_map));
next_map.insert(key, Value::Map(queued_map));
}
None => {
map.insert(key.node, Value::Map(queued_map));
map.insert(key, Value::Map(queued_map));
break;
}
}

View File

@ -1,25 +1,14 @@
use super::{Builtin, GlobalFunctionMap};
use crate::{builtin::builtin_imports::*, evaluate::div};
#[cfg(feature = "random")]
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> {
pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
let num = match args.get_err(0, "number")? {
Value::Dimension(Some(n), Unit::None, _) => Some(n * Number::from(100)),
Value::Dimension(None, Unit::None, _) => None,
v @ Value::Dimension(..) => {
Value::Dimension(SassNumber {
num: n,
unit: Unit::None,
as_slash: _,
}) => n * Number::from(100),
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$number: Expected {} to have no units.",
@ -37,14 +26,29 @@ pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<
.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)?;
match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.round()), u, true)),
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
// todo: better error message, consider finities
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((
format!("$number: {} is not a number.", v.inspect(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)?;
match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.ceil()), u, true)),
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
// todo: better error message, consider finities
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((
format!("$number: {} is not a number.", v.inspect(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)?;
match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.floor()), u, true)),
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
// todo: better error message, consider finities
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((
format!("$number: {} is not a number.", v.inspect(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)?;
match args.get_err(0, "number")? {
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.abs()), u, true)),
Value::Dimension(None, u, ..) => Ok(Value::Dimension(None, u, true)),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => Ok(Value::Dimension(SassNumber {
num: (n.abs()),
unit: u,
as_slash: None,
})),
v => Err((
format!("$number: {} is not a number.", v.inspect(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)?;
let unit1 = match args.get_err(0, "number1")? {
Value::Dimension(_, u, _) => u,
Value::Dimension(SassNumber {
num: _,
unit: u,
as_slash: _,
}) => u,
v => {
return Err((
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")? {
Value::Dimension(_, u, _) => u,
Value::Dimension(SassNumber {
num: _,
unit: u,
as_slash: _,
}) => u,
v => {
return Err((
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
#[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)?;
let limit = match args.default_arg(0, "limit", Value::Null)? {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, u, ..) => {
return Err((format!("$limit: NaN{} is not an int.", u), args.span()).into())
}
Value::Null => {
let mut rng = rand::thread_rng();
return Ok(Value::Dimension(
Some(Number::from(rng.gen_range(0.0..1.0))),
Unit::None,
true,
));
}
v => {
return Err((
format!("$limit: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let limit = args.default_arg(0, "limit", Value::Null);
if limit.is_one() {
return Ok(Value::Dimension(Some(Number::one()), Unit::None, true));
if matches!(limit, Value::Null) {
let mut rng = rand::thread_rng();
return Ok(Value::Dimension(SassNumber {
num: (Number::from(rng.gen_range(0.0..1.0))),
unit: Unit::None,
as_slash: None,
}));
}
if limit.is_decimal() {
return Err((
format!("$limit: {} is not an int.", limit.inspect()),
args.span(),
)
.into());
let limit = limit.assert_number_with_name("limit", args.span())?.num();
let limit_int = limit.assert_int_with_name("limit", args.span())?;
if limit.is_one() {
return Ok(Value::Dimension(SassNumber {
num: (Number::one()),
unit: Unit::None,
as_slash: None,
}));
}
if limit.is_zero() || limit.is_negative() {
@ -164,134 +193,112 @@ pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
.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();
Ok(Value::Dimension(
Some(Number::from(rng.gen_range(0..limit) + 1)),
Unit::None,
true,
))
Ok(Value::Dimension(SassNumber {
num: (Number::from(rng.gen_range(0..limit_int) + 1)),
unit: Unit::None,
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)?;
let span = args.span();
let mut nums = args
.get_variadic()?
.into_iter()
.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()),
})
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()?
.collect::<SassResult<Vec<(Number, Unit)>>>()?
.into_iter();
let mut min = match nums.next() {
Some((Some(n), u)) => (n, u),
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
Some((n, u)) => (n, u),
None => unreachable!(),
};
for (num, unit) in nums {
let num = match num {
Some(n) => n,
None => continue,
};
let lhs = Value::Dimension(SassNumber {
num,
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)
.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()
{
if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::LessThan)?.is_true() {
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)?;
let span = args.span();
let mut nums = args
.get_variadic()?
.into_iter()
.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()),
})
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()?
.collect::<SassResult<Vec<(Number, Unit)>>>()?
.into_iter();
let mut max = match nums.next() {
Some((Some(n), u)) => (n, u),
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
Some((n, u)) => (n, u),
None => unreachable!(),
};
for (num, unit) in nums {
let num = match num {
Some(n) => n,
None => continue,
};
let lhs = Value::Dimension(SassNumber {
num,
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)
.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()
if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::GreaterThan)?.is_true()
{
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)?;
let number1 = args.get_err(0, "number1")?;
let number2 = args.get_err(1, "number2")?;
ValueVisitor::new(parser, args.span()).eval(
HigherIntermediateValue::BinaryOp(
Box::new(HigherIntermediateValue::Literal(number1)),
Op::Div,
Box::new(HigherIntermediateValue::Literal(number2)),
),
true,
)
div(number1, number2, visitor.options, args.span())
}
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::{
args::CallArgs,
common::{Identifier, QuoteKind},
error::SassResult,
parse::Parser,
unit::Unit,
value::{SassFunction, Value},
};
fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?;
if args.get_err(0, "condition")?.is_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)?;
match args.get_err(0, "feature")? {
#[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
// that custom properties are parsed statically, with only
// interpolation treated as SassScript.
"custom-property" => Value::False,
"custom-property" => Value::True,
_ => Value::False,
}),
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)?;
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 => {
return Err((
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))
}
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)?;
let value = args.get_err(0, "value")?;
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)?;
Ok(match args.get_err(0, "number")? {
Value::Dimension(_, Unit::None, _) => Value::True,
Value::Dimension(..) => Value::False,
Value::Dimension(SassNumber {
num: _,
unit: Unit::None,
as_slash: _,
}) => Value::True,
Value::Dimension(SassNumber { .. }) => Value::False,
v => {
return Err((
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)?;
Ok(Value::String(
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)?;
match args.get_err(0, "name")? {
Value::String(s, _) => Ok(Value::bool(
parser.scopes.var_exists(s.into(), parser.global_scope),
)),
Value::String(s, _) => Ok(Value::bool(visitor.env.var_exists(s.into(), None)?)),
v => Err((
format!("$name: {} is not a string.", v.inspect(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)?;
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::Null => None,
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 {
parser
.modules
.get(module_name.into(), args.span())?
.var_exists(name)
(*(*visitor.env.modules)
.borrow()
.get(module_name.into(), args.span())?)
.borrow()
.var_exists(name)
} 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)?;
let name: Identifier = match args.get_err(0, "name")? {
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::Null => None,
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 {
parser
.modules
.get(module_name.into(), args.span())?
.mixin_exists(name)
(*(*visitor.env.modules)
.borrow()
.get(module_name.into(), args.span())?)
.borrow()
.mixin_exists(name)
} 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)?;
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::Null => None,
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 {
parser
.modules
.get(module_name.into(), args.span())?
.fn_exists(name)
(*(*visitor.env.modules)
.borrow()
.get(module_name.into(), args.span())?)
.borrow()
.fn_exists(name)
} 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)?;
let name: Identifier = match args.get_err(0, "name")? {
Value::String(s, _) => s.into(),
@ -227,8 +256,8 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
.into())
}
};
let css = args.default_arg(1, "css", Value::False)?.is_true();
let module = match args.default_arg(2, "module", Value::Null)? {
let css = args.default_arg(1, "css", Value::False).is_true();
let module = match args.default_arg(2, "module", Value::Null) {
Value::String(s, ..) => Some(s),
Value::Null => None,
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 {
return Err((
"$css and $module may not both be passed at once.",
@ -249,67 +278,89 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
.into());
}
parser
.modules
.get(module_name.into(), args.span())?
.get_fn(Spanned {
node: name,
visitor.env.get_fn(
name,
Some(Spanned {
node: module_name.into(),
span: args.span(),
})?
}),
)?
} else {
parser.scopes.get_fn(name, parser.global_scope)
} {
Some(f) => f,
None => match GLOBAL_FUNCTIONS.get(name.as_str()) {
Some(f) => SassFunction::Builtin(f.clone(), name),
None => return Err((format!("Function not found: {}", name), args.span()).into()),
},
match visitor.env.get_fn(name, None)? {
Some(f) => Some(f),
None => GLOBAL_FUNCTIONS
.get(name.as_str())
.map(|f| SassFunction::Builtin(f.clone(), name)),
}
};
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")? {
Value::FunctionRef(f) => f,
v => {
return Err((
format!(
"$function: {} is not a function reference.",
v.inspect(args.span())?
v.inspect(span)?
),
args.span(),
span,
)
.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)]
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)?;
if !parser.flags.in_mixin() {
if !visitor.flags.in_mixin() {
return Err((
"content-exists() may only be called within a mixin.",
parser.span_before,
args.span(),
)
.into());
}
Ok(Value::bool(
parser.content.last().map_or(false, |c| c.content.is_some()),
))
Ok(Value::bool(visitor.env.content.is_some()))
}
#[allow(unused_variables, clippy::needless_pass_by_value)]
pub(crate) fn keywords(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
Err((
"Builtin function `keywords` is not yet implemented",
args.span(),
)
.into())
let span = 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())
}
};
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) {

View File

@ -2,13 +2,13 @@
#![allow(unused_variables)]
use std::{
collections::HashMap,
collections::{BTreeSet, HashMap},
sync::atomic::{AtomicUsize, Ordering},
};
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]
mod macros;
@ -21,16 +21,19 @@ pub mod meta;
pub mod selector;
pub mod string;
// todo: maybe Identifier instead of str?
pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>;
static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0);
// TODO: impl Fn
#[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 {
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);
Self(body, count)
}
@ -55,3 +58,24 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy<GlobalFunctionMap> = Lazy::new(|| {
string::declare(&mut 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::{
args::CallArgs,
common::{Brackets, ListSeparator, QuoteKind},
error::SassResult,
parse::Parser,
selector::{ComplexSelector, ComplexSelectorComponent, Extender, Selector, SelectorList},
value::Value,
use crate::selector::{
ComplexSelector, ComplexSelectorComponent, ExtensionStore, Selector, SelectorList,
};
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)?;
let parent_selector = args
.get_err(0, "super")?
.to_selector(parser, "super", false)?;
let child_selector = args.get_err(1, "sub")?.to_selector(parser, "sub", false)?;
let parent_selector =
args.get_err(0, "super")?
.to_selector(visitor, "super", false, args.span())?;
let child_selector = args
.get_err(1, "sub")?
.to_selector(visitor, "sub", false, args.span())?;
Ok(Value::bool(
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)?;
// todo: Value::to_compound_selector
let selector = args
.get_err(0, "selector")?
.to_selector(parser, "selector", false)?;
let selector =
args.get_err(0, "selector")?
.to_selector(visitor, "selector", false, args.span())?;
if selector.0.components.len() != 1 {
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)?;
Ok(args
.get_err(0, "selector")?
.to_selector(parser, "selector", false)
.to_selector(visitor, "selector", false, args.span())
.map_err(|_| ("$selector: expected selector.", args.span()))?
.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 selectors = args.get_variadic()?;
if selectors.is_empty() {
@ -69,7 +73,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
Ok(selectors
.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>>>()?
.into_iter()
.try_fold(
@ -81,7 +85,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
.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 selectors = args.get_variadic()?;
if selectors.is_empty() {
@ -90,7 +94,7 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
let mut parsed_selectors = selectors
.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>>>()?;
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),
None => {
return Err((
format!("Can't append {} to {}.", complex, parent),
format!(
"Can't append {} to {}.",
complex,
serialize_selector_list(
&parent.0,
visitor.options,
span
)
),
span,
)
.into())
@ -118,7 +130,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
components.extend(complex.components.into_iter().skip(1));
Ok(ComplexSelector::new(components, false))
} 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>>>()?,
@ -129,40 +149,46 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
.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)?;
let selector = args
.get_err(0, "selector")?
.to_selector(parser, "selector", false)?;
let target = args
.get_err(1, "extendee")?
.to_selector(parser, "extendee", false)?;
let source = args
.get_err(2, "extender")?
.to_selector(parser, "extender", false)?;
let selector =
args.get_err(0, "selector")?
.to_selector(visitor, "selector", false, args.span())?;
let target =
args.get_err(1, "extendee")?
.to_selector(visitor, "extendee", false, args.span())?;
let source =
args.get_err(2, "extender")?
.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)?;
let selector = args
.get_err(0, "selector")?
.to_selector(parser, "selector", true)?;
let target = args
.get_err(1, "original")?
.to_selector(parser, "original", true)?;
let source = args
.get_err(2, "replacement")?
.to_selector(parser, "replacement", true)?;
Ok(Extender::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
let selector =
args.get_err(0, "selector")?
.to_selector(visitor, "selector", true, args.span())?;
let target =
args.get_err(1, "original")?
.to_selector(visitor, "original", true, args.span())?;
let source =
args.get_err(2, "replacement")?
.to_selector(visitor, "replacement", true, args.span())?;
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)?;
let selector1 = args
.get_err(0, "selector1")?
.to_selector(parser, "selector1", true)?;
let selector1 =
args.get_err(0, "selector1")?
.to_selector(visitor, "selector1", true, args.span())?;
if selector1.contains_parent_selector() {
return Err((
@ -172,9 +198,9 @@ pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassRes
.into());
}
let selector2 = args
.get_err(1, "selector2")?
.to_selector(parser, "selector2", true)?;
let selector2 =
args.get_err(1, "selector2")?
.to_selector(visitor, "selector2", true, args.span())?;
if selector2.contains_parent_selector() {
return Err((

View File

@ -1,21 +1,6 @@
use super::{Builtin, GlobalFunctionMap};
use crate::builtin::builtin_imports::*;
use num_bigint::BigInt;
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> {
pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
match args.get_err(0, "string")? {
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)?;
match args.get_err(0, "string")? {
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)?;
match args.get_err(0, "string")? {
Value::String(i, _) => Ok(Value::Dimension(
Some(Number::from(i.chars().count())),
Unit::None,
true,
)),
Value::String(i, _) => Ok(Value::Dimension(SassNumber {
num: (Number::from(i.chars().count())),
unit: Unit::None,
as_slash: None,
})),
v => Err((
format!("$string: {} is not a string.", v.inspect(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)?;
match args.get_err(0, "string")? {
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)?;
match args.get_err(0, "string")? {
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)?;
let span = args.span();
let (string, quotes) = match args.get_err(0, "string")? {
Value::String(s, q) => (s, q),
v => {
@ -97,79 +85,46 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
.into())
}
};
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() => {
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() => 1_usize,
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize,
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
.to_usize()
.unwrap(),
Value::Dimension(None, Unit::None, ..) => {
return Err(("NaN is not an int.", args.span()).into())
}
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())
}
let start = args
.get_err(1, "start-at")?
.assert_number_with_name("start-at", span)?;
start.assert_no_units("start-at", span)?;
let start = start.num().assert_int(span)?;
let start = if start == 0 {
1
} else if start > 0 {
(start as usize).min(str_len + 1)
} else {
(start + str_len as i32 + 1).max(1) as usize
};
if end > str_len {
end = str_len;
let end = args
.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 {
Ok(Value::String(String::new(), quotes))
} 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)?;
let s1 = match args.get_err(0, "string")? {
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) {
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,
})
}
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)?;
let span = args.span();
let (s1, quotes) = match args.get_err(0, "string")? {
Value::String(i, q) => (i, q),
v => {
return Err((
format!("$string: {} is not a string.", v.inspect(args.span())?),
args.span(),
format!("$string: {} is not a string.", v.inspect(span)?),
span,
)
.into())
}
@ -231,43 +192,18 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
Value::String(i, _) => i,
v => {
return Err((
format!("$insert: {} is not a string.", v.inspect(args.span())?),
args.span(),
format!("$insert: {} is not a string.", v.inspect(span)?),
span,
)
.into())
}
};
let index = match args.get_err(2, "index")? {
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
return Err((
format!("$index: {} is not an int.", n.inspect()),
args.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())
}
};
let index = args
.get_err(2, "index")?
.assert_number_with_name("index", span)?;
index.assert_no_units("index", span)?;
let index_int = index.num().assert_int_with_name("index", span)?;
if s1.is_empty() {
return Ok(Value::String(substr, quotes));
@ -291,26 +227,13 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
.collect::<String>()
};
let string = if index.is_positive() {
insert(
index
.to_integer()
.to_usize()
.unwrap_or(len + 1)
.min(len + 1)
- 1,
s1,
&substr,
)
} else if index.is_zero() {
let string = if index_int > 0 {
insert((index_int as usize - 1).min(len), s1, &substr)
} else if index_int == 0 {
insert(0, s1, &substr)
} else {
let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1);
if idx > len {
insert(0, s1, &substr)
} else {
insert(len - idx + 1, s1, &substr)
}
let idx = (len as i32 + index_int + 1).max(0) as usize;
insert(idx, s1, &substr)
};
Ok(Value::String(string, quotes))
@ -318,15 +241,15 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
#[cfg(feature = "random")]
#[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)?;
let mut rng = thread_rng();
let string = std::iter::repeat(())
let string: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(7)
.take(12)
.collect();
Ok(Value::String(string, QuoteKind::None))
Ok(Value::String(format!("id-{}", string), QuoteKind::None))
}
pub(crate) fn declare(f: &mut GlobalFunctionMap) {

View File

@ -2,5 +2,32 @@ mod functions;
pub(crate) mod modules;
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::{
list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip},
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) {
f.insert_builtin("append", append);
f.insert_builtin("index", index);
@ -13,4 +37,5 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("nth", nth);
f.insert_builtin("set-nth", set_nth);
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::{
args::CallArgs,
builtin::{
math::{abs, ceil, comparable, divide, floor, max, min, percentage, round},
meta::{unit, unitless},
modules::Module,
},
common::Op,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
use crate::builtin::{
math::{abs, ceil, comparable, divide, floor, max, min, percentage, round},
meta::{unit, unitless},
modules::Module,
};
#[cfg(feature = "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)?;
let span = args.span();
let min = match args.get_err(0, "min")? {
v @ Value::Dimension(..) => v,
v @ Value::Dimension(SassNumber { .. }) => v,
v => {
return Err((
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")? {
v @ Value::Dimension(..) => v,
v @ Value::Dimension(SassNumber { .. }) => v,
v => {
return Err((
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")? {
v @ Value::Dimension(..) => v,
v @ Value::Dimension(SassNumber { .. }) => v,
v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()),
};
// ensure that `min` and `max` are compatible
min.cmp(&max, span, Op::LessThan)?;
min.cmp(&max, span, BinaryOp::LessThan)?;
let min_unit = match min {
Value::Dimension(_, ref u, _) => u,
Value::Dimension(SassNumber {
num: _,
unit: ref u,
as_slash: _,
}) => u,
_ => unreachable!(),
};
let number_unit = match number {
Value::Dimension(_, ref u, _) => u,
Value::Dimension(SassNumber {
num: _,
unit: ref u,
as_slash: _,
}) => u,
_ => unreachable!(),
};
let max_unit = match max {
Value::Dimension(_, ref u, _) => u,
Value::Dimension(SassNumber {
num: _,
unit: ref u,
as_slash: _,
}) => u,
_ => unreachable!(),
};
@ -86,45 +104,43 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
), span).into());
}
match min.cmp(&number, span, Op::LessThan)? {
Ordering::Greater => return Ok(min),
Ordering::Equal => return Ok(number),
Ordering::Less => {}
match min.cmp(&number, span, BinaryOp::LessThan)? {
Some(Ordering::Greater) => return Ok(min),
Some(Ordering::Equal) => return Ok(number),
Some(Ordering::Less) | None => {}
}
match max.cmp(&number, span, Op::GreaterThan)? {
Ordering::Less => return Ok(max),
Ordering::Equal => return Ok(number),
Ordering::Greater => {}
match max.cmp(&number, span, BinaryOp::GreaterThan)? {
Some(Ordering::Less) => return Ok(max),
Some(Ordering::Equal) => return Ok(number),
Some(Ordering::Greater) | None => {}
}
Ok(number)
}
fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.min_args(1)?;
let span = args.span();
let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> {
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()),
}
});
let first: (Number, Unit) = match numbers.next().unwrap()? {
(Some(n), u) => (n.clone() * n, u),
(None, u) => return Ok(Value::Dimension(None, u, true)),
};
let (n, u) = numbers.next().unwrap()?;
let first: (Number, Unit) = (n * n, u);
let rest = numbers
.enumerate()
.map(|(idx, val)| -> SassResult<Option<Number>> {
.map(|(idx, val)| -> SassResult<Number> {
let (number, unit) = val?;
if first.1 == Unit::None {
if unit == Unit::None {
Ok(number.map(|n| n.clone() * n))
Ok(number * number)
} else {
Err((
format!(
@ -149,9 +165,8 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
} else if first.1.comparable(&unit) {
Ok(number
.map(|n| n.convert(&unit, &first.1))
.map(|n| n.clone() * n))
let n = number.convert(&unit, &first.1);
Ok(n * n)
} else {
Err((
format!("Incompatible units {} and {}.", first.1, unit),
@ -160,24 +175,28 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
.into())
}
})
.collect::<SassResult<Option<Vec<Number>>>>()?;
let rest = match rest {
Some(v) => v,
None => return Ok(Value::Dimension(None, first.1, true)),
};
.collect::<SassResult<Vec<Number>>>()?;
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)?;
let number = match args.get_err(0, "number")? {
Value::Dimension(Some(n), Unit::None, ..) => n,
v @ Value::Dimension(Some(..), ..) => {
// Value::Dimension { num: n, .. } if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => num,
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$number: Expected {} to be unitless.",
@ -187,7 +206,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
v @ Value::Dimension(None, ..) => return Ok(v),
v => {
return Err((
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::Dimension(Some(n), Unit::None, ..) => Some(n),
v @ Value::Dimension(Some(..), ..) => {
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => Some(num),
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$number: Expected {} to be unitless.",
@ -210,7 +232,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
v @ Value::Dimension(None, ..) => return Ok(v),
v => {
return Err((
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(
if let Some(base) = base {
Ok(Value::Dimension(SassNumber {
num: if let Some(base) = base {
if base.is_zero() {
Some(Number::zero())
Number::zero()
} else {
number.log(base)
}
} else if number.is_negative() {
None
// todo: test with negative 0
} else if number.is_negative() && !number.is_zero() {
Number(f64::NAN)
} else if number.is_zero() {
todo!()
Number(f64::NEG_INFINITY)
} else {
number.ln()
},
Unit::None,
true,
))
unit: Unit::None,
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)?;
let base = match args.get_err(0, "base")? {
Value::Dimension(Some(n), Unit::None, ..) => n,
v @ Value::Dimension(Some(..), ..) => {
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => num,
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$base: Expected {} to have no units.",
@ -254,7 +280,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
v => {
return Err((
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")? {
Value::Dimension(Some(n), Unit::None, ..) => n,
v @ Value::Dimension(Some(..), ..) => {
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => num,
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$exponent: Expected {} to have no units.",
@ -276,7 +305,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
v => {
return Err((
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)?;
let number = args.get_err(0, "number")?;
Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(n.sqrt(), Unit::None, true),
v @ Value::Dimension(Some(..), ..) => {
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => Value::Dimension(SassNumber {
num: num.sqrt(),
unit: Unit::None,
as_slash: None,
}),
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$number: Expected {} to have no units.",
@ -305,7 +345,6 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
v => {
return Err((
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 {
($name:ident, $name_deg:ident) => {
fn $name(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
($name:ident) => {
fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
let number = args.get_err(0, "number")?;
Ok(match number {
Value::Dimension(Some(n), Unit::None, ..)
| Value::Dimension(Some(n), Unit::Rad, ..) => {
Value::Dimension(n.$name(), Unit::None, true)
}
Value::Dimension(Some(n), Unit::Deg, ..) => {
Value::Dimension(n.$name_deg(), Unit::None, true)
}
v @ Value::Dimension(Some(..), ..) => {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num,
unit: unit @ (Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn),
..
}) => Value::Dimension(SassNumber {
num: Number(coerce_to_rad(num.0, unit).$name()),
unit: Unit::None,
as_slash: None,
}),
v @ Value::Dimension(..) => {
return Err((
format!(
"$number: Expected {} to be an angle.",
"$number: Expected {} to have an angle unit (deg, grad, rad, turn).",
v.inspect(args.span())?
),
args.span(),
)
.into())
}
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
v => {
return Err((
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!(sin, sin_deg);
trig_fn!(tan, tan_deg);
trig_fn!(cos);
trig_fn!(sin);
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)?;
let number = args.get_err(0, "number")?;
Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(
if n > Number::from(1) || n < Number::from(-1) {
None
} else if n.is_one() {
Some(Number::zero())
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => Value::Dimension(SassNumber {
num: if num > Number::from(1) || num < Number::from(-1) {
Number(f64::NAN)
} else if num.is_one() {
Number::zero()
} else {
n.acos()
num.acos()
},
Unit::Deg,
true,
),
v @ Value::Dimension(Some(..), ..) => {
unit: Unit::Deg,
as_slash: None,
}),
v @ Value::Dimension(SassNumber { .. }) => {
return Err((
format!(
"$number: Expected {} to be unitless.",
@ -383,7 +428,6 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
v => {
return Err((
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)?;
let number = args.get_err(0, "number")?;
Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => {
if n > Number::from(1) || n < Number::from(-1) {
return Ok(Value::Dimension(None, Unit::Deg, true));
} else if n.is_zero() {
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true));
Value::Dimension(SassNumber {
num,
unit: Unit::None,
..
}) => {
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((
format!(
"$number: Expected {} to be unitless.",
@ -418,7 +478,6 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
v => {
return Err((
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)?;
let number = args.get_err(0, "number")?;
Ok(match number {
Value::Dimension(Some(n), Unit::None, ..) => {
Value::Dimension(SassNumber {
num: n,
unit: Unit::None,
..
}) => {
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((
format!(
"$number: Expected {} to be unitless.",
@ -451,7 +522,6 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into())
}
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
v => {
return Err((
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)?;
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 => {
return Err((
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")? {
Value::Dimension(n, u, ..) => (n, u),
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => (n, u),
v => {
return Err((
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 = match x_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)
(x_num, y_num)
} else if y_unit == Unit::None {
return Err((
format!(
@ -519,17 +583,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
)
.into());
} else if x_unit.comparable(&y_unit) {
let x = match x_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.convert(&y_unit, &x_unit))
(x_num, y_num.convert(&y_unit, &x_unit))
} else {
return Err((
format!("Incompatible units {} and {}.", y_unit, x_unit),
@ -538,51 +592,11 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
.into());
};
Ok(
match (
NumberState::from_number(&x_num),
NumberState::from_number(&y_num),
) {
(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,
}
}
Ok(Value::Dimension(SassNumber {
num: Number(y_num.0.atan2(x_num.0).to_degrees()),
unit: Unit::Deg,
as_slash: None,
}))
}
pub(crate) fn declare(f: &mut Module) {
@ -614,10 +628,58 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin_var(
"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(
"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::{
args::CallArgs,
builtin::{
meta::{
call, content_exists, feature_exists, function_exists, get_function,
global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists,
},
modules::{Module, ModuleConfig},
use crate::ast::{Configuration, ConfiguredValue};
use crate::builtin::builtin_imports::*;
use crate::builtin::{
meta::{
call, content_exists, feature_exists, function_exists, get_function,
global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists,
},
error::SassResult,
parse::{Parser, Stmt},
value::Value,
modules::Module,
};
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)?;
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::Null => None,
v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()),
};
// todo: tests for `with`
if let Some(with) = with {
let mut config = ModuleConfig::default();
let mut configuration = Configuration::empty();
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 {
let key = match key {
Value::String(s, ..) => s,
let name = match key.node {
Value::String(s, ..) => Identifier::from(s),
v => {
return Err((
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(
Spanned {
node: key.into(),
span,
},
value.span(span),
)?;
if values.contains_key(&name) {
// todo: write test for this
return Err((
format!("The variable {name} was configured twice."),
key.span,
)
.into());
}
values.insert(name, ConfiguredValue::explicit(value, args.span()));
}
let (_, stmts) = parser.load_module(&url, &mut config)?;
Ok(stmts)
} else {
parser.parse_single_import(&url, span)
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: CallArgs, parser: &mut Parser) -> SassResult<Value> {
fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
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(
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)?;
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(
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) {
f.insert_builtin("feature-exists", feature_exists);
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("get-function", get_function);
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);
}

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 crate::{
args::CallArgs,
atrule::mixin::{BuiltinMixin, Mixin},
ast::{ArgumentResult, AstForwardRule, BuiltinMixin, Mixin},
builtin::Builtin,
common::{Identifier, QuoteKind},
common::Identifier,
error::SassResult,
parse::Parser,
scope::Scope,
evaluate::{Environment, Visitor},
selector::ExtensionStore,
utils::{BaseMapView, MapView, MergedMapView, PublicMemberMapView},
value::{SassFunction, SassMap, Value},
};
use super::builtin_imports::QuoteKind;
mod color;
mod list;
mod map;
@ -21,51 +28,89 @@ mod meta;
mod selector;
mod string;
#[derive(Debug, Default)]
pub(crate) struct Module {
pub scope: Scope,
/// A module can itself import other modules
pub modules: Modules,
/// Whether or not this module is builtin
/// e.g. `"sass:math"`
is_builtin: bool,
#[derive(Debug, Clone)]
pub(crate) struct ForwardedModule {
inner: Arc<RefCell<Module>>,
#[allow(dead_code)]
forward_rule: AstForwardRule,
}
#[derive(Debug, Default)]
pub(crate) struct Modules(BTreeMap<Identifier, Module>);
#[derive(Debug, Default)]
pub(crate) struct ModuleConfig(BTreeMap<Identifier, Value>);
impl ModuleConfig {
/// Removes and returns element with name
pub fn get(&mut self, name: Identifier) -> Option<Value> {
self.0.remove(&name)
}
/// If this structure is not empty at the end of
/// an `@use`, we must throw an error
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
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())
impl ForwardedModule {
pub fn if_necessary(
module: Arc<RefCell<Module>>,
rule: AstForwardRule,
) -> Arc<RefCell<Module>> {
if rule.prefix.is_none()
&& rule.shown_mixins_and_functions.is_none()
&& rule.shown_variables.is_none()
&& rule
.hidden_mixins_and_functions
.as_ref()
.map_or(false, HashSet::is_empty)
&& rule
.hidden_variables
.as_ref()
.map_or(false, HashSet::is_empty)
{
module
} 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 {
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) {
return Err((
format!("There's already a module with namespace \"{}\".", name),
@ -79,9 +124,9 @@ impl Modules {
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) {
Some(v) => Ok(v),
Some(v) => Ok(Arc::clone(v)),
None => Err((
format!(
"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) {
Some(v) => Ok(v),
None => Err((
@ -106,153 +155,217 @@ impl Modules {
.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 {
pub fn new_builtin() -> Self {
Module {
scope: Scope::default(),
modules: Modules::default(),
is_builtin: true,
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 get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
if name.node.as_str().starts_with('-') {
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
pub fn new_builtin() -> Self {
Module::Builtin {
scope: ModuleScope::new(),
}
}
match self.scope.vars.get(&name.node) {
fn scope(&self) -> ModuleScope {
match self {
Self::Builtin { scope } | Self::Environment { scope, .. } => scope.clone(),
Self::Forwarded(forwarded) => (*forwarded.inner).borrow().scope(),
}
}
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<Value> {
let scope = self.scope();
match scope.variables.get(name.node) {
Some(v) => Ok(v),
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<()> {
if self.is_builtin {
return Err(("Cannot modify built-in variable.", name.span).into());
let scope = match self {
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(())
} else {
Err(("Undefined variable.", name.span).into())
}
Ok(())
}
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
if name.node.as_str().starts_with('-') {
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
}
let scope = self.scope();
match self.scope.mixins.get(&name.node) {
Some(v) => Ok(v.clone()),
match scope.mixins.get(name.node) {
Some(v) => Ok(v),
None => Err(("Undefined mixin.", name.span).into()),
}
}
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) {
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>> {
if name.node.as_str().starts_with('-') {
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
}
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
let scope = self.scope();
Ok(self.scope.functions.get(&name.node).cloned())
scope.functions.get(name)
}
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 {
!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 {
!name.as_str().starts_with('-') && self.scope.fn_exists(name)
let scope = self.scope();
scope.functions.get(name).is_some()
}
pub fn insert_builtin(
&mut self,
name: &'static str,
function: fn(CallArgs, &mut Parser) -> SassResult<Value>,
function: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
) {
let ident = name.into();
self.scope
let scope = match self {
Self::Builtin { scope } => scope,
_ => unreachable!(),
};
scope
.functions
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
}
pub fn functions(&self) -> SassMap {
pub fn functions(&self, span: Span) -> SassMap {
SassMap::new_with(
self.scope
self.scope()
.functions
.iter()
.into_iter()
.filter(|(key, _)| !key.as_str().starts_with('-'))
.map(|(key, value)| {
(
Value::String(key.to_string(), QuoteKind::Quoted),
Value::FunctionRef(value.clone()),
Value::String(key.to_string(), QuoteKind::Quoted).span(span),
Value::FunctionRef(value),
)
})
.collect::<Vec<(Value, Value)>>(),
.collect::<Vec<_>>(),
)
}
pub fn variables(&self) -> SassMap {
pub fn variables(&self, span: Span) -> SassMap {
SassMap::new_with(
self.scope
.vars
self.scope()
.variables
.iter()
.into_iter()
.filter(|(key, _)| !key.as_str().starts_with('-'))
.map(|(key, value)| {
(
Value::String(key.to_string(), QuoteKind::Quoted),
value.clone(),
Value::String(key.to_string(), QuoteKind::Quoted).span(span),
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 {

View File

@ -15,23 +15,28 @@
//! Named colors retain their original casing,
//! so `rEd` should be emitted as `rEd`.
use std::{
cmp::{max, min},
fmt::{self, Display},
};
use crate::value::Number;
use crate::value::{fuzzy_round, Number};
pub(crate) use name::NAMED_COLORS;
use num_traits::{One, Signed, ToPrimitive, Zero};
mod name;
// todo: only store alpha once on color
#[derive(Debug, Clone)]
pub(crate) struct Color {
rgba: Rgba,
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 {
@ -48,12 +53,12 @@ impl Color {
green: Number,
blue: Number,
alpha: Number,
repr: String,
format: ColorFormat,
) -> Color {
Color {
rgba: Rgba::new(red, green, blue, alpha),
hsla: None,
repr,
format,
}
}
@ -63,12 +68,11 @@ impl Color {
blue: Number,
alpha: Number,
hsla: Hsla,
repr: String,
) -> Color {
Color {
rgba: Rgba::new(red, green, blue, alpha),
hsla: Some(hsla),
repr,
format: ColorFormat::Infer,
}
}
}
@ -84,17 +88,17 @@ struct Rgba {
impl PartialEq for Rgba {
fn eq(&self, other: &Self) -> bool {
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;
}
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;
}
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;
}
@ -120,7 +124,7 @@ impl Rgba {
}
pub fn alpha(&self) -> Number {
self.alpha.clone()
self.alpha
}
}
@ -143,29 +147,29 @@ impl Hsla {
}
pub fn hue(&self) -> Number {
self.hue.clone()
self.hue
}
pub fn saturation(&self) -> Number {
self.saturation.clone()
self.saturation
}
pub fn luminance(&self) -> Number {
self.luminance.clone()
self.luminance
}
pub fn alpha(&self) -> Number {
self.alpha.clone()
self.alpha
}
}
// RGBA color functions
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 {
rgba: Rgba::new(red.into(), green.into(), blue.into(), alpha.into()),
hsla: None,
repr,
format: ColorFormat::Literal(format),
}
}
@ -177,50 +181,62 @@ impl Color {
mut blue: Number,
mut alpha: Number,
) -> Self {
red = red.clamp(0, 255);
green = green.clamp(0, 255);
blue = blue.clamp(0, 255);
alpha = alpha.clamp(0, 1);
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);
let repr = repr(&red, &green, &blue, &alpha);
Color::new_rgba(red, green, blue, alpha, repr)
Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
}
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 {
self.rgba.red.clone().round()
self.rgba.red.round()
}
pub fn blue(&self) -> Number {
self.rgba.blue.clone().round()
self.rgba.blue.round()
}
pub fn green(&self) -> Number {
self.rgba.green.clone().round()
self.rgba.green.round()
}
/// Mix two colors together with weight
/// Algorithm adapted from
/// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718>
pub fn mix(self, other: &Color, weight: Number) -> Self {
let weight = weight.clamp(0, 100);
let normalized_weight = weight.clone() * Number::from(2) - Number::one();
let weight = weight.clamp(0.0, 100.0);
let normalized_weight = weight * Number::from(2.0) - Number::one();
let alpha_distance = self.alpha() - other.alpha();
let combined_weight1 =
if normalized_weight.clone() * alpha_distance.clone() == Number::from(-1) {
normalized_weight
} else {
(normalized_weight.clone() + alpha_distance.clone())
/ (Number::one() + normalized_weight * alpha_distance)
};
let weight1 = (combined_weight1 + Number::one()) / Number::from(2);
let weight2 = Number::one() - weight1.clone();
let combined_weight1 = if normalized_weight * alpha_distance == Number::from(-1) {
normalized_weight
} else {
(normalized_weight + alpha_distance)
/ (Number::one() + normalized_weight * alpha_distance)
};
let weight1 = (combined_weight1 + Number::one()) / Number::from(2.0);
let weight2 = Number::one() - weight1;
Color::from_rgba(
self.red() * weight1.clone() + other.red() * weight2.clone(),
self.green() * weight1.clone() + other.green() * weight2.clone(),
self.red() * weight1 + other.red() * weight2,
self.green() * weight1 + other.green() * 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();
}
let red = self.red() / Number::from(255);
let green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255);
let red = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255.0);
let min = min(&red, min(&green, &blue)).clone();
let max = max(&red, max(&green, &blue)).clone();
let min = red.min(green.min(blue));
let max = red.max(green.max(blue));
let delta = max.clone() - min.clone();
let delta = max - min;
let hue = if min == max {
Number::zero()
} else if max == red {
Number::from(60_u8) * (green - blue) / delta
Number::from(60.0) * (green - blue) / delta
} 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 {
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
pub fn saturation(&self) -> Number {
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 green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255);
let red: Number = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255.0);
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));
if min == max {
return Number::zero();
}
let delta = max.clone() - min.clone();
let delta = max - min;
let sum = max + min;
let s = delta
/ if sum > Number::one() {
Number::from(2) - sum
Number::from(2.0) - sum
} else {
sum
};
s * Number::from(100)
s * Number::from(100.0)
}
/// Calculate luminance from RGBA values
pub fn lightness(&self) -> Number {
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 green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255);
let min = min(&red, min(&green, &blue)).clone();
let red: Number = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255.0);
let min = red.min(green.min(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) {
@ -306,21 +322,21 @@ impl Color {
return (h.hue(), h.saturation(), h.luminance(), h.alpha());
}
let red = self.red() / Number::from(255);
let green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255);
let min = min(&red, min(&green, &blue)).clone();
let max = max(&red, max(&green, &blue)).clone();
let red = self.red() / Number::from(255.0);
let green = self.green() / Number::from(255.0);
let blue = self.blue() / Number::from(255.0);
let min = red.min(green.min(blue));
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 {
Number::zero()
} else {
let d = max.clone() - min.clone();
let mm = max.clone() + min.clone();
let d = max - min;
let mm = max + min;
d / if mm > Number::one() {
Number::from(2) - mm
Number::from(2.0) - mm
} else {
mm
}
@ -329,20 +345,20 @@ impl Color {
let mut hue = if min == max {
Number::zero()
} else if blue == max {
Number::from(4) + (red - green) / (max - min)
Number::from(4.0) + (red - green) / (max - min)
} else if green == max {
Number::from(2) + (blue - red) / (max - min)
Number::from(2.0) + (blue - red) / (max - min)
} else {
(green - blue) / (max - min)
};
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 {
@ -362,90 +378,65 @@ impl Color {
pub fn saturate(&self, amount: Number) -> Self {
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 {
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
pub fn from_hsla(hue: Number, saturation: Number, luminance: 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);
pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self {
let hsla = Hsla::new(
hue.clone(),
saturation.clone(),
luminance.clone(),
alpha.clone(),
hue,
saturation.clamp(0.0, 1.0),
lightness.clamp(0.0, 1.0),
alpha,
);
if saturation.is_zero() {
let val = luminance * Number::from(255);
let repr = repr(&val, &val, &val, &alpha);
return Color::new_hsla(val.clone(), val.clone(), val, alpha, hsla, repr);
}
let scaled_hue = hue.0 / 360.0;
let scaled_saturation = saturation.0.clamp(0.0, 1.0);
let scaled_lightness = lightness.0.clamp(0.0, 1.0);
let temporary_1 = if luminance < Number::small_ratio(1, 2) {
luminance.clone() * (Number::one() + saturation)
let m2 = if scaled_lightness <= 0.5 {
scaled_lightness * (scaled_saturation + 1.0)
} 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 {
($temp:ident) => {
if $temp > Number::one() {
$temp -= Number::one();
} else if $temp.is_negative() {
$temp += Number::one();
}
};
let m1 = scaled_lightness.mul_add(2.0, -m2);
let red = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue + 1.0 / 3.0) * 255.0);
let green = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue) * 255.0);
let blue = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue - 1.0 / 3.0) * 255.0);
Color::new_hsla(Number(red), Number(green), Number(blue), alpha, hsla)
}
fn hue_to_rgb(m1: f64, m2: f64, mut hue: f64) -> f64 {
if hue < 0.0 {
hue += 1.0;
}
if hue > 1.0 {
hue -= 1.0;
}
clamp_temp!(temporary_r);
clamp_temp!(temporary_g);
clamp_temp!(temporary_b);
fn channel(temp: Number, temp1: &Number, temp2: &Number) -> Number {
Number::from(255)
* if Number::from(6) * temp.clone() < Number::one() {
temp2.clone() + (temp1.clone() - temp2.clone()) * Number::from(6) * temp
} else if Number::from(2) * temp.clone() < Number::one() {
temp1.clone()
} 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 {
temp2.clone()
}
if hue < 1.0 / 6.0 {
((m2 - m1) * hue).mul_add(6.0, m1)
} else if hue < 1.0 / 2.0 {
m2
} else if hue < 2.0 / 3.0 {
((m2 - m1) * (2.0 / 3.0 - hue)).mul_add(6.0, m1)
} else {
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 {
@ -456,9 +447,8 @@ impl Color {
let red = Number::from(u8::max_value()) - self.red();
let green = Number::from(u8::max_value()) - self.green();
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)
}
@ -475,7 +465,7 @@ impl Color {
pub fn alpha(&self) -> Number {
let a = self.rgba.alpha();
if a > Number::one() {
a / Number::from(255)
a / Number::from(255.0)
} else {
a
}
@ -505,108 +495,41 @@ impl Color {
impl Color {
pub fn to_ie_hex_str(&self) -> String {
format!(
"#{:X}{:X}{:X}{:X}",
(self.alpha() * Number::from(255)).round().to_integer(),
self.red().to_integer(),
self.green().to_integer(),
self.blue().to_integer()
"#{:02X}{:02X}{:02X}{:02X}",
fuzzy_round(self.alpha().0 * 255.0) as u8,
self.red().0 as u8,
self.green().0 as u8,
self.blue().0 as u8
)
}
}
/// HWB color functions
impl Color {
pub fn from_hwb(
mut hue: Number,
mut white: Number,
mut black: Number,
mut alpha: Number,
) -> Color {
hue %= Number::from(360);
hue /= Number::from(360);
white /= Number::from(100);
black /= Number::from(100);
alpha = alpha.clamp(Number::zero(), Number::one());
pub fn from_hwb(hue: Number, white: Number, black: Number, mut alpha: Number) -> Color {
let hue = Number(hue.rem_euclid(360.0) / 360.0);
let mut scaled_white = white.0 / 100.0;
let mut scaled_black = black.0 / 100.0;
alpha = alpha.clamp(0.0, 1.0);
let white_black_sum = white.clone() + black.clone();
let white_black_sum = scaled_white + scaled_black;
if white_black_sum > Number::one() {
white /= white_black_sum.clone();
black /= white_black_sum;
if white_black_sum > 1.0 {
scaled_white /= 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 {
if hue < Number::zero() {
hue += Number::one();
}
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 to_rgb = |hue: f64| -> Number {
let channel = Self::hue_to_rgb(0.0, 1.0, hue).mul_add(factor, scaled_white);
Number(fuzzy_round(channel * 255.0))
};
let red = to_rgb(hue.clone() + Number::small_ratio(1, 3));
let green = to_rgb(hue.clone());
let blue = to_rgb(hue - Number::small_ratio(1, 3));
let red = to_rgb(hue.0 + 1.0 / 3.0);
let green = to_rgb(hue.0);
let blue = to_rgb(hue.0 - 1.0 / 3.0);
let repr = repr(&red, &green, &blue, &alpha);
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)
Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
}
}

View File

@ -3,7 +3,16 @@ use std::fmt::{self, Display, Write};
use crate::interner::InternedString;
#[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,
NotEqual,
GreaterThan,
@ -14,51 +23,43 @@ pub enum Op {
Minus,
Mul,
Div,
// todo: maybe rename mod, since it is mod
Rem,
And,
Or,
Not,
}
impl Display for Op {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl BinaryOp {
pub fn precedence(self) -> u8 {
match self {
Self::Equal => write!(f, "=="),
Self::NotEqual => write!(f, "!="),
Self::GreaterThanEqual => write!(f, ">="),
Self::LessThanEqual => write!(f, "<="),
Self::GreaterThan => write!(f, ">"),
Self::LessThan => write!(f, "<"),
Self::Plus => write!(f, "+"),
Self::Minus => write!(f, "-"),
Self::Mul => write!(f, "*"),
Self::Div => write!(f, "/"),
Self::Rem => write!(f, "%"),
Self::And => write!(f, "and"),
Self::Or => write!(f, "or"),
Self::Not => write!(f, "not"),
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 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 {
impl Display for BinaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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,
BinaryOp::SingleEq => write!(f, "="),
BinaryOp::Equal => write!(f, "=="),
BinaryOp::NotEqual => write!(f, "!="),
BinaryOp::GreaterThanEqual => write!(f, ">="),
BinaryOp::LessThanEqual => write!(f, "<="),
BinaryOp::GreaterThan => write!(f, ">"),
BinaryOp::LessThan => write!(f, "<"),
BinaryOp::Plus => write!(f, "+"),
BinaryOp::Minus => write!(f, "-"),
BinaryOp::Mul => write!(f, "*"),
BinaryOp::Div => write!(f, "/"),
BinaryOp::Rem => write!(f, "%"),
BinaryOp::And => write!(f, "and"),
BinaryOp::Or => write!(f, "or"),
}
}
}
@ -85,31 +86,47 @@ pub(crate) enum Brackets {
Bracketed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, Eq)]
pub(crate) enum ListSeparator {
Space,
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 {
pub fn as_str(self) -> &'static str {
match self {
Self::Space => " ",
Self::Space | Self::Undecided => " ",
Self::Comma => ", ",
Self::Slash => " / ",
}
}
pub fn as_compressed_str(self) -> &'static str {
match self {
Self::Space => " ",
Self::Space | Self::Undecided => " ",
Self::Comma => ",",
Self::Slash => "/",
}
}
pub fn name(self) -> &'static str {
match self {
Self::Space => "space",
Self::Space | Self::Undecided => "space",
Self::Comma => "comma",
Self::Slash => "slash",
}
}
}
@ -119,9 +136,17 @@ impl ListSeparator {
///
/// This struct protects that invariant by normalizing all
/// 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);
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 {
fn from_str(s: &str) -> Self {
if s.contains('_') {
@ -130,6 +155,10 @@ impl Identifier {
Identifier(InternedString::get_or_intern(s))
}
}
pub fn is_public(&self) -> bool {
!self.as_str().starts_with('-')
}
}
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) {
match self.kind {
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,
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::Raw(..) => unreachable!(),
};
let first_bar = if unicode { '╷' } else { '|' };
let first_bar = if unicode { '╷' } else { ',' };
let second_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 col = loc.begin.column + 1;
@ -148,7 +148,12 @@ impl Display for SassError {
.collect::<String>()
)?;
writeln!(f, "{}{}", padding, fourth_bar)?;
writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
if unicode {
writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
} else {
writeln!(f, " {} {}:{} root stylesheet", loc.file.name(), line, col)?;
}
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::path::Path;
use std::{
io::{self, Error, ErrorKind},
path::Path,
};
/// 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.
fn is_file(&self, path: &Path) -> bool;
/// 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.
@ -36,7 +38,7 @@ impl Fs for StdFs {
}
#[inline]
fn read(&self, path: &Path) -> Result<Vec<u8>> {
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
}
@ -61,7 +63,7 @@ impl Fs for NullFs {
}
#[inline]
fn read(&self, _path: &Path) -> Result<Vec<u8>> {
fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
Err(Error::new(
ErrorKind::NotFound,
"NullFs, there is no file system",

View File

@ -23,6 +23,7 @@ impl InternedString {
self.resolve_ref() == ""
}
// todo: no need for unsafe here
pub fn resolve_ref<'a>(self) -> &'a str {
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 codemap::File;
use codemap::{File, Span};
use crate::Token;
@ -10,53 +10,70 @@ const FORM_FEED: char = '\x0C';
pub(crate) struct Lexer<'a> {
buf: Cow<'a, [Token]>,
cursor: usize,
amt_peeked: usize,
}
impl<'a> Lexer<'a> {
fn peek_cursor(&self) -> usize {
self.cursor + self.amt_peeked
pub fn raw_text(&self, start: usize) -> String {
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> {
self.buf.get(self.peek_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()
self.buf.get(self.cursor).copied()
}
/// Peeks the previous token without modifying the peek cursor
pub fn peek_previous(&mut self) -> Option<Token> {
self.buf.get(self.peek_cursor().checked_sub(1)?).copied()
}
pub fn peek_forward(&mut self, n: usize) -> Option<Token> {
self.amt_peeked += n;
self.peek()
self.buf.get(self.cursor.checked_sub(1)?).copied()
}
/// Peeks `n` from current peeked position without modifying cursor
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> {
self.amt_peeked = self.amt_peeked.checked_sub(n)?;
self.peek()
/// Peeks `n` behind current peeked position without modifying cursor
pub fn peek_n_backwards(&self, n: usize) -> Option<Token> {
self.buf.get(self.cursor.checked_sub(n)?).copied()
}
/// Set cursor to position and reset peek
pub fn set_cursor(&mut self, cursor: usize) {
self.cursor = cursor;
self.amt_peeked = 0;
}
pub fn cursor(&self) -> usize {
@ -70,7 +87,6 @@ impl<'a> Iterator for Lexer<'a> {
fn next(&mut self) -> Option<Self::Item> {
self.buf.get(self.cursor).copied().map(|tok| {
self.cursor += 1;
self.amt_peeked = self.amt_peeked.saturating_sub(1);
tok
})
}
@ -122,15 +138,6 @@ impl<'a> Lexer<'a> {
Lexer {
buf: Cow::Owned(buf),
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.
Spec progress as of 0.11.2, released on 2022-09-03:
| Passing | Failing | Total |
|---------|---------|-------|
| 4205 | 2051 | 6256 |
/*!
This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS.
## Use as library
```
fn main() -> Result<(), Box<grass::Error>> {
let sass = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?;
assert_eq!(sass, "a b {\n color: a b;\n}\n");
let css = grass::from_string(
"a { b { color: &; } }".to_owned(),
&grass::Options::default()
)?;
assert_eq!(css, "a b {\n color: a b;\n}\n");
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)]
#![allow(
clippy::use_self,
@ -57,200 +54,54 @@ grass input.scss
clippy::items_after_statements,
// this is only available on nightly
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 parse::{CssParser, SassParser, StylesheetParser};
use serializer::Serializer;
#[cfg(feature = "wasm-exports")]
use wasm_bindgen::prelude::*;
pub(crate) use beef::lean::Cow;
use codemap::CodeMap;
pub use crate::error::{
PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
};
pub use crate::fs::{Fs, NullFs, StdFs};
pub(crate) use crate::token::Token;
use crate::{
builtin::modules::{ModuleConfig, Modules},
lexer::Lexer,
output::{AtRuleContext, Css},
parse::{
common::{ContextFlags, NeverEmptyVec},
Parser,
},
scope::{Scope, Scopes},
selector::{ExtendedSelector, Extender, SelectorList},
};
pub use crate::options::{InputSyntax, Options, OutputStyle};
pub(crate) use crate::{context_flags::ContextFlags, token::Token};
use crate::{evaluate::Visitor, lexer::Lexer, parse::ScssParser};
mod args;
mod atrule;
mod ast;
mod builtin;
mod color;
mod common;
mod context_flags;
mod error;
mod evaluate;
mod fs;
mod interner;
mod lexer;
mod output;
mod options;
mod parse;
mod scope;
mod selector;
mod style;
mod serializer;
mod token;
mod unit;
mod utils;
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> {
let (message, span) = err.raw();
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 file = map.add_file(file_name.to_owned(), input);
let empty_span = file.span.subspan(0, 0);
let lexer = Lexer::new_from_file(&file);
let stmts = Parser {
toks: &mut Lexer::new_from_file(&file),
map: &mut map,
path: file_name.as_ref(),
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(ExtendedSelector::new(SelectorList::new(
empty_span,
))),
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(),
let path = Path::new(file_name);
let input_syntax = options
.input_syntax
.unwrap_or_else(|| InputSyntax::for_path(path));
let stylesheet = match input_syntax {
InputSyntax::Scss => {
ScssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__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)),
}
.parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
let stmts = visitor.finish();
Css::from_stmts(stmts, AtRuleContext::None, options.allows_charset)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?
.pretty_print(&map, options.style)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
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))?;
prev_was_group_end = is_group_end;
prev_requires_semicolon = requires_semicolon;
}
Ok(serializer.finish(prev_requires_semicolon))
}
/// Compile CSS from a path
@ -300,8 +176,8 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
/// Ok(())
/// }
/// ```
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
#[inline]
pub fn from_path(p: &str, options: &Options) -> Result<String> {
from_string_with_file_name(
String::from_utf8(options.fs.read(Path::new(p))?)?,
@ -319,8 +195,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
/// Ok(())
/// }
/// ```
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
#[inline]
pub fn from_string(input: String, options: &Options) -> Result<String> {
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<()> {
let matches = App::new("grass")
.setting(AppSettings::ColoredHelp)
@ -144,6 +143,12 @@ fn main() -> std::io::Result<()> {
.hidden(true)
.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::with_name("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 crate::{
atrule::keyframes::{Keyframes, KeyframesSelector},
error::SassResult,
lexer::Lexer,
parse::Stmt,
Token,
};
use crate::{ast::KeyframesSelector, error::SassResult, lexer::Lexer, token::Token};
use super::{common::ContextFlags, Parser};
use super::BaseParser;
impl fmt::Display for KeyframesSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -20,201 +14,128 @@ impl fmt::Display for KeyframesSelector {
}
}
struct KeyframesSelectorParser<'a, 'b, 'c> {
parser: &'a mut Parser<'b, 'c>,
pub(crate) struct KeyframesSelectorParser<'a> {
toks: Lexer<'a>,
}
impl<'a, 'b, 'c> KeyframesSelectorParser<'a, 'b, 'c> {
pub fn new(parser: &'a mut Parser<'b, 'c>) -> Self {
Self { parser }
impl<'a> BaseParser<'a> for KeyframesSelectorParser<'a> {
fn toks(&self) -> &Lexer<'a> {
&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();
self.parser.whitespace_or_comment();
while let Some(tok) = self.parser.toks.peek() {
match tok.kind {
't' | 'T' => {
let mut ident = self.parser.parse_identifier()?;
ident.node.make_ascii_lowercase();
if ident.node == "to" {
selectors.push(KeyframesSelector::To);
} else {
return Err(("Expected \"to\" or \"from\".", tok.pos).into());
}
loop {
self.whitespace()?;
if self.looking_at_identifier() {
if self.scan_identifier("to", false)? {
selectors.push(KeyframesSelector::To);
} else if self.scan_identifier("from", false)? {
selectors.push(KeyframesSelector::From);
} else {
return Err(("Expected \"to\" or \"from\".", self.toks.current_span()).into());
}
'f' | 'F' => {
let mut ident = self.parser.parse_identifier()?;
ident.node.make_ascii_lowercase();
if ident.node == "from" {
selectors.push(KeyframesSelector::From);
} else {
return Err(("Expected \"to\" or \"from\".", tok.pos).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 {
selectors.push(self.parse_percentage_selector()?);
}
self.whitespace()?;
if !self.scan_char(',') {
break;
}
}
Ok(selectors)
}
}
impl<'a, 'b> Parser<'a, 'b> {
fn parse_keyframes_name(&mut self) -> SassResult<String> {
let mut name = String::new();
self.whitespace_or_comment();
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),
}
fn parse_percentage_selector(&mut self) -> SassResult<KeyframesSelector> {
let mut buffer = String::new();
if self.scan_char('+') {
buffer.push('+');
}
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());
};
if !matches!(
self.toks.peek(),
Some(Token {
kind: '0'..='9' | '.',
..
})
) {
return Err(("Expected number.", self.toks.current_span()).into());
}
self.span_before = span;
while matches!(
self.toks.peek(),
Some(Token {
kind: '0'..='9',
..
})
) {
buffer.push(self.toks.next().unwrap().kind);
}
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();
if self.scan_char('.') {
buffer.push('.');
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 Ok(selector);
}
c => string.push(c),
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');
pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult<Stmt> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
if matches!(
self.toks.peek(),
Some(Token {
kind: '+' | '-',
..
})
) {
buffer.push(self.toks.next().unwrap().kind);
}
if !matches!(
self.toks.peek(),
Some(Token {
kind: '0'..='9',
..
})
) {
return Err(("Expected digit.", self.toks.current_span()).into());
}
while matches!(
self.toks.peek(),
Some(Token {
kind: '0'..='9',
..
})
) {
buffer.push(self.toks.next().unwrap().kind);
}
}
let name = self.parse_keyframes_name()?;
self.expect_char('%')?;
self.whitespace();
let body = 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_KEYFRAMES,
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 })))
Ok(KeyframesSelector::Percent(buffer.into_boxed_str()))
}
}

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