diff --git a/.github/ISSUE_TEMPLATE/incorrect-sass-output.md b/.github/ISSUE_TEMPLATE/incorrect-sass-output.md index 1e5b93d2..79bf5550 100644 --- a/.github/ISSUE_TEMPLATE/incorrect-sass-output.md +++ b/.github/ISSUE_TEMPLATE/incorrect-sass-output.md @@ -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; } ``` + **`grass` Output**: ``` a { diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 306941d0..64c7a684 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c638bdbe..edcedc45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 0bf6819a..601361e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 +# 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" diff --git a/README.md b/README.md index b8006646..bcf56544 100644 --- a/README.md +++ b/README.md @@ -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 ``` + + \ No newline at end of file diff --git a/benches/big_for.scss b/benches/big_for.scss deleted file mode 100644 index 8d6cff4b..00000000 --- a/benches/big_for.scss +++ /dev/null @@ -1,5 +0,0 @@ -@for $i from 0 to 250 { - a { - color: $i; - } -} \ No newline at end of file diff --git a/benches/colors.rs b/benches/colors.rs deleted file mode 100644 index d96a8c86..00000000 --- a/benches/colors.rs +++ /dev/null @@ -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); diff --git a/benches/control_flow.rs b/benches/control_flow.rs deleted file mode 100644 index 0f50deef..00000000 --- a/benches/control_flow.rs +++ /dev/null @@ -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); diff --git a/benches/many_floats.scss b/benches/many_floats.scss deleted file mode 100644 index 65499860..00000000 --- a/benches/many_floats.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/benches/many_foo.scss b/benches/many_foo.scss deleted file mode 100644 index 5f3117c4..00000000 --- a/benches/many_foo.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/benches/many_hsla.scss b/benches/many_hsla.scss deleted file mode 100644 index 8ca17f8e..00000000 --- a/benches/many_hsla.scss +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/benches/many_integers.scss b/benches/many_integers.scss deleted file mode 100644 index d64dddc7..00000000 --- a/benches/many_integers.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/benches/many_named_colors.scss b/benches/many_named_colors.scss deleted file mode 100644 index 013efbae..00000000 --- a/benches/many_named_colors.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/benches/many_small_integers.scss b/benches/many_small_integers.scss deleted file mode 100644 index 83d90ece..00000000 --- a/benches/many_small_integers.scss +++ /dev/null @@ -1,1001 +0,0 @@ -a { - color: 3253997; - color: 2003956; - color: 2242572; - color: 6413001; - color: 3540432; - color: 4145183; - color: 482383; - color: 8515372; - color: 3758095; - color: 4482743; - color: 598424; - color: 0762713; - color: 8587326; - color: 3436469; - color: 8145259; - color: 5395122; - color: 4431363; - color: 8181984; - color: 3515695; - color: 714530; - color: 9639279; - color: 1926818; - color: 0207917; - color: 927322; - color: 5796423; - color: 7844098; - color: 7051158; - color: 2578722; - color: 4176911; - color: 3407165; - color: 1966552; - color: 1132629; - color: 0725911; - color: 0153730; - color: 7718497; - color: 5603758; - color: 4804059; - color: 046510; - color: 0556987; - color: 2907748; - color: 4228447; - color: 8879151; - color: 7659201; - color: 9591544; - color: 9028486; - color: 76789; - color: 4275054; - color: 5535619; - color: 1843734; - color: 1692997; - color: 9549077; - color: 4071220; - color: 4786908; - color: 4532421; - color: 7358017; - color: 9510509; - color: 1252378; - color: 1653966; - color: 9070087; - color: 6624652; - color: 5042183; - color: 4463594; - color: 4540312; - color: 7716829; - color: 8243673; - color: 9327572; - color: 9161676; - color: 2731569; - color: 8125990; - color: 6125794; - color: 3017386; - color: 9109374; - color: 9331173; - color: 8918903; - color: 3353253; - color: 2275592; - color: 3356603; - color: 579466; - color: 9570578; - color: 0811399; - color: 0994936; - color: 3476491; - color: 255402; - color: 6713059; - color: 9500728; - color: 777484; - color: 6486431; - color: 8886553; - color: 8010659; - color: 6466740; - color: 5672699; - color: 6104676; - color: 6170346; - color: 8841623; - color: 1888390; - color: 1911856; - color: 1187881; - color: 6976060; - color: 4267639; - color: 3682009; - color: 9913049; - color: 0090614; - color: 1820377; - color: 6062140; - color: 2031408; - color: 5711717; - color: 5736947; - color: 6032420; - color: 0517133; - color: 8689934; - color: 3366891; - color: 8691098; - color: 0648435; - color: 3097706; - color: 67639; - color: 8385653; - color: 2617124; - color: 5360961; - color: 6749883; - color: 9140367; - color: 8609482; - color: 218943; - color: 4523880; - color: 3659358; - color: 9323533; - color: 5030538; - color: 2745271; - color: 3657626; - color: 5717771; - color: 7272361; - color: 7189128; - color: 1306398; - color: 1791640; - color: 5930448; - color: 0277284; - color: 5239750; - color: 554611; - color: 7658532; - color: 6715313; - color: 7143005; - color: 7898532; - color: 0012475; - color: 6592968; - color: 2350555; - color: 6516636; - color: 727127; - color: 7195782; - color: 5125045; - color: 5206861; - color: 0807994; - color: 0249658; - color: 3301; - color: 3089209; - color: 8333200; - color: 4696257; - color: 0432822; - color: 9755313; - color: 2503780; - color: 4260898; - color: 9878498; - color: 0795361; - color: 347584; - color: 1222149; - color: 0421254; - color: 4105330; - color: 5220890; - color: 5408761; - color: 8008656; - color: 6284590; - color: 7032089; - color: 3412142; - color: 9345344; - color: 6555434; - color: 7857546; - color: 4106841; - color: 5567884; - color: 4075368; - color: 7418698; - color: 7921693; - color: 1953321; - color: 2753774; - color: 9825547; - color: 4816755; - color: 7262438; - color: 0135571; - color: 1335199; - color: 8788289; - color: 9527755; - color: 7498884; - color: 2353140; - color: 6318791; - color: 3576800; - color: 1860026; - color: 597524; - color: 3271688; - color: 595005; - color: 9418735; - color: 8401438; - color: 6792180; - color: 4521355; - color: 1149893; - color: 9199334; - color: 1065374; - color: 3934680; - color: 5567525; - color: 6938148; - color: 7208342; - color: 2904124; - color: 3270949; - color: 8972756; - color: 7342576; - color: 8503088; - color: 338912; - color: 632508; - color: 9734201; - color: 1588468; - color: 8117354; - color: 6728092; - color: 9387478; - color: 1692360; - color: 0681224; - color: 7408612; - color: 9747455; - color: 2805134; - color: 9131321; - color: 2329191; - color: 1743398; - color: 8987029; - color: 7378729; - color: 2271331; - color: 4576055; - color: 0848044; - color: 2888421; - color: 9491186; - color: 8618888; - color: 2495485; - color: 5077255; - color: 553026; - color: 8149392; - color: 7945340; - color: 4130221; - color: 9882925; - color: 4386041; - color: 1616690; - color: 6476117; - color: 6241596; - color: 9601630; - color: 7155111; - color: 3128175; - color: 7548821; - color: 6971359; - color: 0699745; - color: 9695866; - color: 8517197; - color: 0943777; - color: 9760854; - color: 3243671; - color: 6015829; - color: 3253097; - color: 5070972; - color: 5871859; - color: 4721486; - color: 9146317; - color: 7661879; - color: 7009200; - color: 5705527; - color: 4228694; - color: 565492; - color: 8643010; - color: 3264596; - color: 9167198; - color: 0238123; - color: 3768859; - color: 7982676; - color: 8900238; - color: 3837658; - color: 4557073; - color: 7427745; - color: 8181195; - color: 1510244; - color: 6288547; - color: 2320838; - color: 3301273; - color: 493613; - color: 2668984; - color: 6017824; - color: 7849201; - color: 6836563; - color: 8783147; - color: 5980217; - color: 9235510; - color: 0672608; - color: 9336546; - color: 1951556; - color: 3866989; - color: 0169532; - color: 1236665; - color: 7244334; - color: 2918069; - color: 2212400; - color: 7418379; - color: 6570814; - color: 8243162; - color: 7363314; - color: 3489554; - color: 9150419; - color: 5115127; - color: 2925187; - color: 7762012; - color: 0242068; - color: 9722832; - color: 4646435; - color: 1574138; - color: 9987552; - color: 2994621; - color: 9031251; - color: 3624188; - color: 6230113; - color: 3937389; - color: 3457722; - color: 5913603; - color: 4288031; - color: 4607961; - color: 0131939; - color: 5644258; - color: 2260541; - color: 7472245; - color: 6003539; - color: 9109802; - color: 5087212; - color: 0748567; - color: 89245; - color: 3852982; - color: 8582434; - color: 7601712; - color: 8044926; - color: 1593261; - color: 7691091; - color: 6779850; - color: 6713754; - color: 6282762; - color: 6104048; - color: 839182; - color: 6477632; - color: 7522476; - color: 7097829; - color: 3248674; - color: 0380744; - color: 7810443; - color: 6388854; - color: 0990847; - color: 7734346; - color: 0360671; - color: 4669040; - color: 0778650; - color: 7230367; - color: 4705063; - color: 7523379; - color: 316216; - color: 5994334; - color: 7820640; - color: 2306043; - color: 5966175; - color: 0261194; - color: 3123106; - color: 8857531; - color: 3446740; - color: 7482815; - color: 325139; - color: 2324635; - color: 0679593; - color: 7897784; - color: 9272230; - color: 2570624; - color: 3253900; - color: 3739311; - color: 6728665; - color: 3852646; - color: 4609964; - color: 9936888; - color: 6344471; - color: 5333181; - color: 6792841; - color: 9980105; - color: 0199795; - color: 4888115; - color: 307086; - color: 2096738; - color: 5601644; - color: 2403987; - color: 2244905; - color: 9963921; - color: 5468735; - color: 9648174; - color: 2714349; - color: 2717654; - color: 914950; - color: 9546210; - color: 5828014; - color: 0594600; - color: 8974771; - color: 5255799; - color: 7218309; - color: 0838691; - color: 4364178; - color: 1783773; - color: 6441496; - color: 6749356; - color: 4235201; - color: 984328; - color: 971020; - color: 2389182; - color: 0755200; - color: 9683272; - color: 3389093; - color: 2599809; - color: 5169097; - color: 2774114; - color: 9237645; - color: 0314552; - color: 128030; - color: 1455512; - color: 9369687; - color: 2856963; - color: 9188341; - color: 9012341; - color: 2421311; - color: 6285604; - color: 67871; - color: 5660557; - color: 7119856; - color: 6685919; - color: 4967467; - color: 2556166; - color: 6999518; - color: 3218240; - color: 7010112; - color: 4770899; - color: 9836; - color: 3713610; - color: 7229070; - color: 3139154; - color: 430883; - color: 4018326; - color: 1449916; - color: 1845613; - color: 2279699; - color: 5497544; - color: 2200525; - color: 4910480; - color: 5802254; - color: 4515883; - color: 8368404; - color: 8851244; - color: 3533164; - color: 055008; - color: 0293105; - color: 1808357; - color: 7762044; - color: 629276; - color: 3569532; - color: 920919; - color: 2244414; - color: 5007050; - color: 0230097; - color: 3868007; - color: 88635; - color: 0332343; - color: 0844317; - color: 5372298; - color: 5528300; - color: 8156718; - color: 4114397; - color: 334800; - color: 9378011; - color: 3187642; - color: 866430; - color: 68812; - color: 6890900; - color: 4979611; - color: 5599925; - color: 0540946; - color: 4591480; - color: 7296964; - color: 001587; - color: 3192643; - color: 9463897; - color: 6546659; - color: 7407582; - color: 1265553; - color: 004233; - color: 7192506; - color: 1844182; - color: 3845928; - color: 8018427; - color: 6966456; - color: 3857552; - color: 9957389; - color: 2353119; - color: 9676113; - color: 5992894; - color: 0415922; - color: 597702; - color: 0720938; - color: 8216494; - color: 7812198; - color: 5381718; - color: 6639723; - color: 7096519; - color: 9583692; - color: 6887420; - color: 5132124; - color: 470963; - color: 3582047; - color: 9233073; - color: 2272387; - color: 5276283; - color: 8413650; - color: 0063047; - color: 5360094; - color: 7168354; - color: 3999152; - color: 067658; - color: 9023408; - color: 8402802; - color: 040568; - color: 7529604; - color: 5432159; - color: 1888805; - color: 7649276; - color: 9826809; - color: 3722467; - color: 5917840; - color: 695303; - color: 8255592; - color: 0497236; - color: 1974270; - color: 2405314; - color: 5244319; - color: 3893248; - color: 7724916; - color: 4647773; - color: 6182067; - color: 6848920; - color: 7735721; - color: 9924220; - color: 5363311; - color: 2835007; - color: 5960616; - color: 8628034; - color: 4860745; - color: 5457095; - color: 5853312; - color: 1960422; - color: 9406233; - color: 1833259; - color: 9301093; - color: 6967223; - color: 8293485; - color: 4881693; - color: 8331825; - color: 03578; - color: 7444854; - color: 2750568; - color: 4235872; - color: 4649752; - color: 9780312; - color: 9027015; - color: 4097734; - color: 9435958; - color: 9972469; - color: 0337424; - color: 6404945; - color: 4261540; - color: 1348744; - color: 3376151; - color: 8704786; - color: 3969792; - color: 8890927; - color: 7119751; - color: 7470751; - color: 544130; - color: 528105; - color: 8620602; - color: 4460208; - color: 9675429; - color: 7287214; - color: 3244327; - color: 6037604; - color: 4133431; - color: 8471831; - color: 6059008; - color: 5345379; - color: 1082199; - color: 0895674; - color: 2180285; - color: 0977672; - color: 7380738; - color: 1167762; - color: 8929161; - color: 0839621; - color: 4013229; - color: 1721542; - color: 1434657; - color: 8476581; - color: 7840138; - color: 0333490; - color: 9411331; - color: 9957183; - color: 7579107; - color: 2841406; - color: 5122508; - color: 5520099; - color: 7787628; - color: 2774215; - color: 8031711; - color: 8934963; - color: 8961974; - color: 8571142; - color: 9326673; - color: 5675471; - color: 926024; - color: 866393; - color: 6107177; - color: 6072043; - color: 3468992; - color: 475971; - color: 0942720; - color: 9411206; - color: 838272; - color: 8008967; - color: 488408; - color: 651764; - color: 3232291; - color: 6327061; - color: 2452157; - color: 4097104; - color: 3157595; - color: 3712472; - color: 1561120; - color: 3837952; - color: 5056000; - color: 5348201; - color: 0829340; - color: 1468306; - color: 1946050; - color: 46117; - color: 3972488; - color: 4732964; - color: 0600626; - color: 5163787; - color: 5013720; - color: 7595655; - color: 0524250; - color: 4632406; - color: 4437046; - color: 6422753; - color: 3986235; - color: 8479813; - color: 738350; - color: 0624980; - color: 0672944; - color: 2061010; - color: 3648160; - color: 8584690; - color: 3251517; - color: 1318256; - color: 7017183; - color: 5029582; - color: 4822552; - color: 5086059; - color: 8374282; - color: 3037570; - color: 3565133; - color: 9947553; - color: 3798700; - color: 2815591; - color: 5060384; - color: 052596; - color: 1464290; - color: 7459893; - color: 6830666; - color: 6724587; - color: 2453175; - color: 8657733; - color: 4153489; - color: 9949739; - color: 2222114; - color: 7103532; - color: 6229646; - color: 2770715; - color: 7156319; - color: 4465394; - color: 0154858; - color: 0139078; - color: 1994551; - color: 9214184; - color: 3974364; - color: 3852202; - color: 2288571; - color: 6640312; - color: 2589410; - color: 7912449; - color: 3687049; - color: 2240911; - color: 3953953; - color: 5686190; - color: 5433697; - color: 0225088; - color: 8132451; - color: 5493679; - color: 7681989; - color: 87538; - color: 3592925; - color: 1686512; - color: 2811183; - color: 3008311; - color: 738613; - color: 9128235; - color: 8494077; - color: 7212333; - color: 7742390; - color: 1997633; - color: 5499175; - color: 2667116; - color: 2583986; - color: 9074638; - color: 3024243; - color: 6312593; - color: 9353543; - color: 5299258; - color: 7096515; - color: 0490472; - color: 2455894; - color: 1390120; - color: 1217456; - color: 8106527; - color: 901169; - color: 3614006; - color: 7202683; - color: 6555758; - color: 7584195; - color: 5083311; - color: 5045895; - color: 3288510; - color: 4516981; - color: 6583485; - color: 1416630; - color: 4797998; - color: 8593660; - color: 5554037; - color: 6329448; - color: 0917333; - color: 6265947; - color: 6660770; - color: 50592; - color: 6896922; - color: 2009633; - color: 2846298; - color: 6535132; - color: 6822333; - color: 3956589; - color: 7288413; - color: 1730071; - color: 8596997; - color: 7966708; - color: 0958945; - color: 4090243; - color: 0569082; - color: 1283155; - color: 9176807; - color: 0579006; - color: 1809156; - color: 2116127; - color: 6323568; - color: 0454614; - color: 7052103; - color: 1039112; - color: 4291181; - color: 6884618; - color: 8493689; - color: 062051; - color: 8227549; - color: 204974; - color: 3299674; - color: 2933180; - color: 791426; - color: 5217123; - color: 0577521; - color: 6770593; - color: 3709796; - color: 8442994; - color: 9556426; - color: 773179; - color: 4485369; - color: 7285745; - color: 9858209; - color: 2640443; - color: 2662104; - color: 7886596; - color: 340678; - color: 0829775; - color: 3722234; - color: 5830612; - color: 617986; - color: 2287349; - color: 9695396; - color: 7833817; - color: 504888; - color: 0927076; - color: 7781532; - color: 5171036; - color: 6334840; - color: 304078; - color: 2679765; - color: 4476471; - color: 8714377; - color: 0694170; - color: 405854; - color: 5413772; - color: 1119025; - color: 05552; - color: 2620629; - color: 8196485; - color: 0165685; - color: 4051349; - color: 5741462; - color: 9239151; - color: 0793312; - color: 0935037; - color: 2968404; - color: 4816072; - color: 6509266; - color: 5624254; - color: 778474; - color: 2263059; - color: 5400580; - color: 4509315; - color: 4763401; - color: 6362756; - color: 6341901; - color: 0884392; - color: 1848543; - color: 3304862; - color: 9614373; - color: 1197799; - color: 8224452; - color: 6810173; - color: 5677769; - color: 2191177; - color: 1275313; - color: 477558; - color: 9675823; - color: 9518945; - color: 5735144; - color: 7964135; - color: 5439595; - color: 6280221; - color: 1814694; - color: 2653752; - color: 9597112; - color: 1741485; - color: 264756; - color: 8172244; - color: 9990453; - color: 9423512; - color: 9601137; - color: 9503568; - color: 2187560; - color: 2130883; - color: 6557193; - color: 5606344; - color: 7992543; - color: 5274076; - color: 3504998; - color: 524673; - color: 6221048; - color: 2802259; - color: 7838333; - color: 2803303; - color: 7941425; - color: 1117446; - color: 1012379; - color: 1946734; - color: 0431364; - color: 2255737; - color: 1245659; - color: 0991286; - color: 7786896; - color: 4639547; - color: 3238702; - color: 6054186; - color: 0163165; - color: 0560594; - color: 7077672; - color: 5811412; - color: 466691; - color: 6040033; - color: 9348889; - color: 6528770; - color: 7184024; - color: 0708751; - color: 2840523; - color: 4175213; - color: 5742103; - color: 0276880; - color: 2482140; - color: 0131199; - color: 7557375; - color: 2410918; - color: 8530896; - color: 5292552; - color: 9599111; - color: 7064620; - color: 472269; - color: 2147871; - color: 149593; - color: 2552327; - color: 2467751; - color: 7409863; - color: 2786180; - color: 8196888; - color: 8324295; - color: 2362756; - color: 3490709; - color: 0038437; - color: 4136586; - color: 1486418; - color: 3530028; - color: 0262421; - color: 7565465; - color: 249609; - color: 0134430; - color: 6270829; - color: 160667; - color: 9238685; - color: 4925176; - color: 9737608; - color: 6006459; - color: 911059; - color: 6362808; - color: 6243082; - color: 0487166; - color: 9568438; - color: 5708514; - color: 1625018; - color: 0287720; - color: 4123777; - color: 0227287; - color: 8500439; - color: 6530043; - color: 5588749; - color: 1127162; - color: 8660885; - color: 5161354; - color: 7205126; - color: 2404414; - color: 8922697; - color: 0157324; - color: 684034; - color: 717224; - color: 316958; - color: 2868018; - color: 5809624; - color: 0299407; - color: 418210; - color: 2899509; - color: 6619415; - color: 2729618; - color: 8917882; - color: 4093802; - color: 3688660; - color: 6423172; - color: 2020286; - color: 3544896; - color: 2838477; - color: 2636607; - color: 2016433; - color: 119646; - color: 2547765; - color: 121434; - color: 5980478; - color: 6983066; - color: 1577902; - color: 8116124; - color: 9033921; - color: 8683942; - color: 3164589; - color: 8347314; - color: 6889654; - color: 5431620; - color: 4555567; - color: 8592365; - color: 5858101; - color: 6563062; - color: 4235538; - color: 1261939; - color: 3387167; - color: 5255981; -} \ No newline at end of file diff --git a/benches/many_variable_redeclarations.scss b/benches/many_variable_redeclarations.scss deleted file mode 100644 index 3c2442fc..00000000 --- a/benches/many_variable_redeclarations.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/benches/numbers.rs b/benches/numbers.rs deleted file mode 100644 index 023bc018..00000000 --- a/benches/numbers.rs +++ /dev/null @@ -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); diff --git a/benches/styles.rs b/benches/styles.rs deleted file mode 100644 index a26102f2..00000000 --- a/benches/styles.rs +++ /dev/null @@ -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); diff --git a/benches/variables.rs b/benches/variables.rs deleted file mode 100644 index f986fbd5..00000000 --- a/benches/variables.rs +++ /dev/null @@ -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); diff --git a/input.scss b/input.scss index 67ce83e4..0ac7fcef 100644 --- a/input.scss +++ b/input.scss @@ -1,3 +1,3 @@ -body { - background: red; -} +a { + color: red; +} \ No newline at end of file diff --git a/sass-spec b/sass-spec index e3489596..f7265276 160000 --- a/sass-spec +++ b/sass-spec @@ -1 +1 @@ -Subproject commit e348959657f1e274cef658283436a311a925a673 +Subproject commit f7265276e53b0c5e6df0f800ed4b0ae61fbd0351 diff --git a/src/args.rs b/src/args.rs deleted file mode 100644 index 04641d56..00000000 --- a/src/args.rs +++ /dev/null @@ -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); - -#[derive(Debug, Clone)] -pub(crate) struct FuncArg { - pub name: Identifier, - pub default: Option>, - 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>>, pub Span); - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub(crate) enum CallArg { - Named(Identifier), - Positional(usize), -} - -impl CallArg { - pub fn position(&self) -> Result { - 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> { - 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::>>>()? - .join(", "), - ); - string.push(')'); - Ok(Spanned { node: string, span }) - } - - /// Get argument by name - /// - /// Removes the argument - pub fn get_named>(&mut self, val: T) -> Option>> { - 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>> { - self.0.remove(&CallArg::Positional(val)) - } - - pub fn get>( - &mut self, - position: usize, - name: T, - ) -> Option>> { - 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 { - 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 { - Ok(match self.get(position, name) { - Some(val) => val?.node, - None => default, - }) - } - - pub fn positional_arg(&mut self, position: usize) -> Option>> { - self.get_positional(position) - } - - pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> SassResult { - Ok(match self.get_named(name) { - Some(val) => val?.node, - None => default, - }) - } - - pub fn get_variadic(self) -> SassResult>> { - let mut vals = Vec::new(); - let mut args = match self - .0 - .into_iter() - .map(|(a, v)| Ok((a.position()?, v))) - .collect::>)>, 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) - } -} diff --git a/src/ast/args.rs b/src/ast/args.rs new file mode 100644 index 00000000..c581d551 --- /dev/null +++ b/src/ast/args.rs @@ -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, +} + +#[derive(Debug, Clone)] +pub(crate) struct ArgumentDeclaration { + pub args: Vec, + pub rest: Option, +} + +impl ArgumentDeclaration { + pub fn empty() -> Self { + Self { + args: Vec::new(), + rest: None, + } + } + + pub fn verify( + &self, + num_positional: usize, + names: &BTreeMap, + 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::>(); + + 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, + pub named: BTreeMap, + pub rest: Option, + pub keyword_rest: Option, + 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, + pub named: BTreeMap, + pub separator: ListSeparator, + pub span: Span, + // todo: hack + pub touched: BTreeSet, +} + +impl ArgumentResult { + /// Get argument by name + /// + /// Removes the argument + pub fn get_named>(&mut self, val: T) -> Option> { + 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> { + 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>(&mut self, position: usize, name: T) -> Option> { + 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 { + 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 { + 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>> { + 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) + } +} diff --git a/src/ast/css.rs b/src/ast/css.rs new file mode 100644 index 00000000..54e48743 --- /dev/null +++ b/src/ast/css.rs @@ -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, + 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), +} + +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, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) enum KeyframesSelector { + To, + From, + Percent(Box), +} + +#[derive(Debug, Clone)] +pub(crate) struct SupportsRule { + pub params: String, + pub body: Vec, +} diff --git a/src/ast/expr.rs b/src/ast/expr.rs new file mode 100644 index 00000000..e73d3cb8 --- /dev/null +++ b/src/ast/expr.rs @@ -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>, + pub separator: ListSeparator, + pub brackets: Brackets, +} + +#[derive(Debug, Clone)] +pub(crate) struct FunctionCallExpr { + pub namespace: Option>, + pub name: Identifier, + pub arguments: Box, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub(crate) struct InterpolatedFunction { + pub name: Interpolation, + pub arguments: Box, + pub span: Span, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct AstSassMap(pub Vec<(Spanned, AstExpr)>); + +#[derive(Debug, Clone)] +pub(crate) enum AstExpr { + BinaryOp { + lhs: Box, + op: BinaryOp, + rhs: Box, + allows_slash: bool, + span: Span, + }, + True, + False, + Calculation { + name: CalculationName, + args: Vec, + }, + Color(Box), + FunctionCall(FunctionCallExpr), + If(Box), + InterpolatedFunction(InterpolatedFunction), + List(ListExpr), + Map(AstSassMap), + Null, + Number { + n: Number, + unit: Unit, + }, + Paren(Box), + ParentSelector, + String(StringExpr, Span), + Supports(Box), + UnaryOp(UnaryOp, Box, Span), + Variable { + name: Spanned, + namespace: Option>, + }, +} + +// 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) -> 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 { + Spanned { node: self, span } + } +} diff --git a/src/ast/interpolation.rs b/src/ast/interpolation.rs new file mode 100644 index 00000000..7fadefd3 --- /dev/null +++ b/src/ast/interpolation.rs @@ -0,0 +1,99 @@ +use codemap::Spanned; + +use crate::token::Token; + +use super::AstExpr; + +#[derive(Debug, Clone)] +pub(crate) struct Interpolation { + pub contents: Vec, +} + +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) -> 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) { + 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), +} diff --git a/src/atrule/media.rs b/src/ast/media.rs similarity index 62% rename from src/atrule/media.rs rename to src/ast/media.rs index d0507eae..62875f9f 100644 --- a/src/atrule/media.rs +++ b/src/ast/media.rs @@ -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, + pub query: Vec, + pub body: Vec, } #[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, - - /// The media type, for example "screen" or "print". - /// - /// This may be `None`. If so, `self.features` will not be empty. pub media_type: Option, - - /// Feature queries, including parentheses. - pub features: Vec, + pub conditions: Vec, + 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) -> Self { + pub fn condition( + conditions: Vec, + // default=true + conjunction: bool, + ) -> Self { Self { modifier: None, media_type: None, - features, + conditions, + conjunction, + } + } + + pub fn media_type( + media_type: Option, + modifier: Option, + conditions: Option>, + ) -> Self { + Self { + modifier, + conjunction: true, + media_type, + conditions: conditions.unwrap_or_default(), } } + pub fn parse_list(list: &str, span: Span) -> SassResult> { + 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), diff --git a/src/ast/mixin.rs b/src/ast/mixin.rs new file mode 100644 index 00000000..6988571b --- /dev/null +++ b/src/ast/mixin.rs @@ -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(), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs new file mode 100644 index 00000000..5ec3781e --- /dev/null +++ b/src/ast/mod.rs @@ -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; diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs new file mode 100644 index 00000000..cbcad486 --- /dev/null +++ b/src/ast/stmt.rs @@ -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, + #[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, + pub else_clause: Option>, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstIfClause { + pub condition: AstExpr, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstFor { + pub variable: Spanned, + pub from: Spanned, + pub to: Spanned, + pub is_exclusive: bool, + pub body: Vec, +} + +#[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, + pub selector_span: Span, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstStyle { + pub name: Interpolation, + pub value: Option>, + pub body: Vec, + 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, + pub list: AstExpr, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstMedia { + pub query: Interpolation, + pub body: Vec, + pub span: Span, +} + +pub(crate) type CssMediaQuery = MediaQuery; + +#[derive(Debug, Clone)] +pub(crate) struct AstWhile { + pub condition: AstExpr, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstVariableDecl { + pub namespace: Option>, + 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, + pub arguments: ArgumentDeclaration, + pub children: Vec, +} + +#[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, + /// 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, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstInclude { + pub namespace: Option>, + pub name: Spanned, + pub args: ArgumentInvocation, + pub content: Option, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstUnknownAtRule { + pub name: Interpolation, + pub value: Option, + pub children: Option>, + 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, + pub query: Option, + #[allow(unused)] + pub span: Span, +} + +#[derive(Debug, Clone)] +pub(crate) struct AtRootQuery { + pub include: bool, + pub names: HashSet, + pub all: bool, + pub rule: bool, +} + +impl AtRootQuery { + pub fn new(include: bool, names: HashSet) -> 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, +} + +#[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, + pub configuration: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub(crate) struct ConfiguredVariable { + pub name: Spanned, + pub expr: Spanned, + pub is_guarded: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct Configuration { + pub values: Arc>, + #[allow(unused)] + pub original_config: Option>>, + pub span: Option, +} + +impl Configuration { + pub fn through_forward( + config: Arc>, + forward: &AstForwardRule, + ) -> Arc> { + 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>, + values: Arc>, + ) -> Self { + Self { + values, + original_config: Some(config), + span: None, + } + } + + pub fn first(&self) -> Option> { + 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 { + self.values.remove(name) + } + + pub fn is_implicit(&self) -> bool { + self.span.is_none() + } + + pub fn implicit(values: BTreeMap) -> Self { + Self { + values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))), + original_config: None, + span: None, + } + } + + pub fn explicit(values: BTreeMap, 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>) -> Arc> { + 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, +} + +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>, + pub shown_variables: Option>, + pub hidden_mixins_and_functions: Option>, + pub hidden_variables: Option>, + pub prefix: Option, + pub configuration: Vec, + pub span: Span, +} + +impl AstForwardRule { + pub fn new( + url: PathBuf, + prefix: Option, + configuration: Option>, + 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, + shown_variables: HashSet, + prefix: Option, + configuration: Option>, + 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, + hidden_variables: HashSet, + prefix: Option, + configuration: Option>, + 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), + Operation { + left: Box, + operator: Option, + right: Box, + }, +} + +#[derive(Debug, Clone)] +pub(crate) struct AstSupportsRule { + pub condition: AstSupportsCondition, + pub children: Vec, + 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, + pub url: PathBuf, + pub is_plain_css: bool, + pub uses: Vec, + pub forwards: Vec, +} + +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(), + } + } +} diff --git a/src/ast/style.rs b/src/ast/style.rs new file mode 100644 index 00000000..89ed14d9 --- /dev/null +++ b/src/ast/style.rs @@ -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>, + pub declared_as_custom_property: bool, +} diff --git a/src/atrule/unknown.rs b/src/ast/unknown.rs similarity index 71% rename from src/atrule/unknown.rs rename to src/ast/unknown.rs index 51a8e5ea..f360e96c 100644 --- a/src/atrule/unknown.rs +++ b/src/ast/unknown.rs @@ -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, + pub body: Vec, /// Whether or not this @-rule was declared with curly /// braces. A body may not necessarily have contents diff --git a/src/atrule/function.rs b/src/atrule/function.rs deleted file mode 100644 index f0b69aac..00000000 --- a/src/atrule/function.rs +++ /dev/null @@ -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, - pub declared_at_root: bool, - pos: Span, -} - -impl Hash for Function { - fn hash(&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, declared_at_root: bool, pos: Span) -> Self { - Function { - args, - body, - declared_at_root, - pos, - } - } -} diff --git a/src/atrule/keyframes.rs b/src/atrule/keyframes.rs deleted file mode 100644 index ea7e2462..00000000 --- a/src/atrule/keyframes.rs +++ /dev/null @@ -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, -} - -#[derive(Debug, Clone)] -pub(crate) struct KeyframesRuleSet { - pub selector: Vec, - pub body: Vec, -} - -#[derive(Debug, Clone)] -pub(crate) enum KeyframesSelector { - To, - From, - Percent(Box), -} diff --git a/src/atrule/kind.rs b/src/atrule/kind.rs deleted file mode 100644 index 9aebbaa0..00000000 --- a/src/atrule/kind.rs +++ /dev/null @@ -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> for AtRuleKind { - type Error = Box; - fn try_from(c: &Spanned) -> Result> { - 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()), - }) - } -} diff --git a/src/atrule/mixin.rs b/src/atrule/mixin.rs deleted file mode 100644 index a5f010ef..00000000 --- a/src/atrule/mixin.rs +++ /dev/null @@ -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>; - -#[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, - 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, - pub accepts_content_block: bool, - pub declared_at_root: bool, -} - -impl UserDefinedMixin { - pub fn new( - args: FuncArgs, - body: Vec, - 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>, - - /// Optional args, e.g. `@content(a, b, c);` - pub content_args: Option, - - /// 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, -} diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs deleted file mode 100644 index 80304d02..00000000 --- a/src/atrule/mod.rs +++ /dev/null @@ -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; diff --git a/src/atrule/supports.rs b/src/atrule/supports.rs deleted file mode 100644 index 145c3ddd..00000000 --- a/src/atrule/supports.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::parse::Stmt; - -#[derive(Debug, Clone)] -pub(crate) struct SupportsRule { - pub params: String, - pub body: Vec, -} diff --git a/src/builtin/functions/color/hsl.rs b/src/builtin/functions/color/hsl.rs index 5c3fd27d..fe20a8e7 100644 --- a/src/builtin/functions/color/hsl.rs +++ b/src/builtin/functions/color/hsl.rs @@ -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 { - 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 { + let span = args.span(); - let len = args.len(); + 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 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 [&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, + )); + } - if channels.len() > 3 { - return Err(( - format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() - ), - args.span(), - ) - .into()); - } + 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, + )?; - if channels.iter().any(Value::is_special_function) { - let channel_sep = if channels.len() < 3 { - ListSeparator::Space - } else { - ListSeparator::Comma - }; + 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), + )))) +} - return Ok(Value::String( - format!( - "{}({})", - name, - Value::List(channels, channel_sep, Brackets::None) - .to_css_string(args.span(), false)? - ), - QuoteKind::None, - )); - } +fn inner_hsl( + name: &'static str, + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { + args.max_args(4)?; + let span = args.span(); - 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 len = args.len(); - 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()), - }; + 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(), + }; - 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()) + hsl_3_args(name, args, visitor) } - 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 { - inner_hsl("hsl", args, parser) +pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + inner_hsl("hsl", args, visitor) } -pub(crate) fn hsla(args: CallArgs, parser: &mut Parser) -> SassResult { - inner_hsl("hsla", args, parser) +pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + inner_hsl("hsla", args, visitor) } -pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 } } -pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn saturation(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn lightness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { .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 { +fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { } }; 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 { Ok(Value::Color(Box::new(color.darken(amount)))) } -fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult { +fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { }; 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 { Ok(Value::Color(Box::new(color.saturate(amount)))) } -fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult { +fn desaturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { } }; 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 { Ok(Value::Color(Box::new(color.desaturate(amount)))) } -pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn grayscale(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn complement(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn invert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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 { - Ok(Value::String(format!("invert(NaN{})", u), QuoteKind::None)) - } v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), diff --git a/src/builtin/functions/color/hwb.rs b/src/builtin/functions/color/hwb.rs index a1c09e57..29d7943e 100644 --- a/src/builtin/functions/color/hwb.rs +++ b/src/builtin/functions/color/hwb.rs @@ -1,15 +1,8 @@ -use num_traits::One; - -use crate::{ - args::CallArgs, - color::Color, - error::SassResult, - parse::Parser, - unit::Unit, - value::{Number, Value}, -}; - -pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult { +use crate::builtin::builtin_imports::*; + +use super::rgb::{parse_channels, ParsedChannels}; + +pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { - 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 { + 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 .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 .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 { + 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) + } +} diff --git a/src/builtin/functions/color/mod.rs b/src/builtin/functions/color/mod.rs index 815b9b54..3c6005f2 100644 --- a/src/builtin/functions/color/mod.rs +++ b/src/builtin/functions/color/mod.rs @@ -1,4 +1,4 @@ -use super::{Builtin, GlobalFunctionMap}; +use super::GlobalFunctionMap; pub mod hsl; pub mod hwb; diff --git a/src/builtin/functions/color/opacity.rs b/src/builtin/functions/color/opacity.rs index 05f0c70b..60f284ae 100644 --- a/src/builtin/functions/color/opacity.rs +++ b/src/builtin/functions/color/opacity.rs @@ -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 { +pub(crate) fn alpha(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn opacity(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, @@ -99,74 +105,16 @@ fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult { .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_in(amount)))) -} + let amount = args + .get_err(1, "amount")? + .assert_number_with_name("amount", args.span())?; -fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult { - 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_in(amount)))) -} + let amount = bound!(args, "amount", amount.num(), amount.unit(), 0, 1); -// todo: unify with `fade_out` -fn transparentize(mut args: CallArgs, parser: &mut Parser) -> SassResult { - 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)))) + Ok(Value::Color(Box::new(color.fade_in(amount)))) } -fn fade_out(mut args: CallArgs, parser: &mut Parser) -> SassResult { +fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, @@ -179,8 +127,12 @@ fn fade_out(mut args: CallArgs, parser: &mut Parser) -> SassResult { } }; 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)); } diff --git a/src/builtin/functions/color/other.rs b/src/builtin/functions/color/other.rs index 7d16b72f..12507ca0 100644 --- a/src/builtin/functions/color/other.rs +++ b/src/builtin/functions/color/other.rs @@ -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 { - if args.positional_arg(1).is_some() { +pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + 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 { +pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, diff --git a/src/builtin/functions/color/rgb.rs b/src/builtin/functions/color/rgb.rs index 0f590485..b5a94c5f 100644 --- a/src/builtin/functions/color/rgb.rs +++ b/src/builtin/functions/color/rgb.rs @@ -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 { + let args = args + .iter() + .map(|arg| arg.to_css_string(span, visitor.options.is_compressed())) + .collect::>>()? + .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 { - 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 { + // 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)?, + ))))) +} + +fn inner_rgb_3_arg( + name: &'static str, + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { + let alpha = if args.len() > 3 { + args.get(3, "alpha") + } else { + None + }; - 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()), + 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 { + 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), +} + +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 { + 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, - )); + if !alpha_from_slash_list + .as_ref() + .map(Value::is_special_function) + .unwrap_or(false) + { + inner_alpha_from_slash_list + .clone() + .assert_number_with_name("alpha", span)?; } - 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()), - }; + alpha_from_slash_list = Some(inner_alpha_from_slash_list); - 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()), - }; + if list[0].is_var() { + let fn_string = function_string(name, &[original_channels], visitor, span)?; + return Ok(ParsedChannels::String(fn_string)); + } + } - let color = Color::from_rgba(red, green, blue, Number::one()); + let is_comma_separated = channels.separator() == ListSeparator::Comma; + let is_bracketed = matches!(channels, Value::List(_, _, Brackets::Bracketed)); - 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 is_comma_separated || is_bracketed { + let mut err_buffer = "$channels must be".to_owned(); - 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, - )); + if is_bracketed { + err_buffer.push_str(" an unbracketed"); } - 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 is_comma_separated { + if is_bracketed { + err_buffer.push(','); + } else { + err_buffer.push_str(" a"); } - }; - 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, - )); + err_buffer.push_str(" space-separated"); } - 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) + 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())) { - 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, - )); + 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()); } + } - let red = match red { - 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!( - "$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()) + if let Some(alpha_from_slash_list) = alpha_from_slash_list { + list.push(alpha_from_slash_list); + return Ok(ParsedChannels::List(list)); + } + + #[allow(clippy::collapsible_match)] + match &list[2] { + Value::Dimension(SassNumber { as_slash, .. }) => match as_slash { + Some(slash) => Ok(ParsedChannels::List(vec![ + list[0].clone(), + list[1].clone(), + // todo: superfluous clones + Value::Dimension(slash.0.clone()), + Value::Dimension(slash.1.clone()), + ])), + None => Ok(ParsedChannels::List(list)), + }, + Value::String(text, QuoteKind::None) if text.contains('/') => { + let fn_string = function_string(name, &[channels], visitor, span)?; + Ok(ParsedChannels::String(fn_string)) + } + _ => Ok(ParsedChannels::List(list)), + } +} + +/// name: Either `rgb` or `rgba` depending on the caller +fn inner_rgb( + name: &'static str, + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { + 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) + } } - }; - Ok(Value::Color(Box::new(Color::from_rgba( - red, green, blue, alpha, - )))) + } + 2 => inner_rgb_2_arg(name, args, visitor), + _ => inner_rgb_3_arg(name, args, visitor), } } -pub(crate) fn rgb(args: CallArgs, parser: &mut Parser) -> SassResult { - inner_rgb("rgb", args, parser) +pub(crate) fn rgb(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + inner_rgb("rgb", args, visitor) } -pub(crate) fn rgba(args: CallArgs, parser: &mut Parser) -> SassResult { - inner_rgb("rgba", args, parser) +pub(crate) fn rgba(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + inner_rgb("rgba", args, visitor) } -pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn red(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 } } -pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn green(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn blue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 } } -pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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(), ) diff --git a/src/builtin/functions/list.rs b/src/builtin/functions/list.rs index 2d81e885..32dfc7be 100644 --- a/src/builtin/functions/list.rs +++ b/src/builtin/functions/list.rs @@ -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 { +pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 .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 { +pub(crate) fn list_separator(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 (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 SassResult { +pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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 SassResult { +pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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 } "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 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 Ok(Value::List(list1, sep, brackets)) } -pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn is_bracketed(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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 { +pub(crate) fn zip(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let lists = args .get_variadic()? .into_iter() diff --git a/src/builtin/functions/macros.rs b/src/builtin/functions/macros.rs index 605c50bf..3f02cfa0 100644 --- a/src/builtin/functions/macros.rs +++ b/src/builtin/functions/macros.rs @@ -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 {}{}.", diff --git a/src/builtin/functions/map.rs b/src/builtin/functions/map.rs index db75afd0..d42105a3 100644 --- a/src/builtin/functions/map.rs +++ b/src/builtin/functions/map.rs @@ -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 { +pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { - 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 SassResult { +pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { - 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; } } diff --git a/src/builtin/functions/math.rs b/src/builtin/functions/math.rs index 0a8558f1..7e6a0d78 100644 --- a/src/builtin/functions/math.rs +++ b/src/builtin/functions/math.rs @@ -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 { +pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn round(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn ceil(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 } } -pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn floor(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn abs(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 } } -pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn comparable(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn random(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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 { +pub(crate) fn min(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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::, Unit)>>>()? + .collect::>>()? .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 { +pub(crate) fn max(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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::, Unit)>>>()? + .collect::>>()? .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 { +pub(crate) fn divide(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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) { diff --git a/src/builtin/functions/meta.rs b/src/builtin/functions/meta.rs index d115c67b..4917e301 100644 --- a/src/builtin/functions/meta.rs +++ b/src/builtin/functions/meta.rs @@ -1,17 +1,28 @@ -use super::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS}; +use crate::builtin::builtin_imports::*; -use codemap::Spanned; - -use crate::{ - args::CallArgs, - common::{Identifier, QuoteKind}, - error::SassResult, - parse::Parser, - unit::Unit, - value::{SassFunction, Value}, -}; +// 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, + } +} -fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult { +fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { } } -pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn feature_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn unit(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 Ok(Value::String(unit, QuoteKind::Quoted)) } -pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser) -> SassResult { +pub(crate) fn type_of(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn variable_exists( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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 { +pub(crate) fn global_variable_exists( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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 { +pub(crate) fn mixin_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn function_exists( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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 { +pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + 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 { +pub(crate) fn content_exists(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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) { diff --git a/src/builtin/functions/mod.rs b/src/builtin/functions/mod.rs index f5a3968b..5ee70489 100644 --- a/src/builtin/functions/mod.rs +++ b/src/builtin/functions/mod.rs @@ -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, usize); +pub(crate) struct Builtin( + pub fn(ArgumentResult, &mut Visitor) -> SassResult, + usize, +); impl Builtin { - pub fn new(body: fn(CallArgs, &mut Parser) -> SassResult) -> Builtin { + pub fn new(body: fn(ArgumentResult, &mut Visitor) -> SassResult) -> Builtin { let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed); Self(body, count) } @@ -55,3 +58,24 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy = Lazy::new(|| { string::declare(&mut m); m }); + +pub(crate) static DISALLOWED_PLAIN_CSS_FUNCTION_NAMES: Lazy> = Lazy::new(|| { + GLOBAL_FUNCTIONS + .keys() + .copied() + .filter(|&name| { + !matches!( + name, + "rgb" + | "rgba" + | "hsl" + | "hsla" + | "grayscale" + | "invert" + | "alpha" + | "opacity" + | "saturate" + ) + }) + .collect() +}); diff --git a/src/builtin/functions/selector.rs b/src/builtin/functions/selector.rs index 91e0243b..730923d4 100644 --- a/src/builtin/functions/selector.rs +++ b/src/builtin/functions/selector.rs @@ -1,32 +1,36 @@ -use super::{Builtin, GlobalFunctionMap}; - -use crate::{ - args::CallArgs, - common::{Brackets, ListSeparator, QuoteKind}, - error::SassResult, - parse::Parser, - selector::{ComplexSelector, ComplexSelectorComponent, Extender, Selector, SelectorList}, - value::Value, +use crate::builtin::builtin_imports::*; + +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 { +pub(crate) fn is_superselector( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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 { +pub(crate) fn simple_selectors( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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 { +pub(crate) fn selector_parse(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn selector_nest(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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>>()? .into_iter() .try_fold( @@ -81,7 +85,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult SassResult { +pub(crate) fn selector_append(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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::>>()?; 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::>>()?, @@ -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 { +pub(crate) fn selector_extend( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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)?; - - Ok(Extender::extend(selector.0, source.0, target.0, args.span())?.to_sass_list()) + 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(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 { +pub(crate) fn selector_replace( + mut args: ArgumentResult, + visitor: &mut Visitor, +) -> SassResult { 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 { +pub(crate) fn selector_unify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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(( diff --git a/src/builtin/functions/string.rs b/src/builtin/functions/string.rs index 3a778b24..09baeadc 100644 --- a/src/builtin/functions/string.rs +++ b/src/builtin/functions/string.rs @@ -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 { +pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn to_lower_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn str_length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { +pub(crate) fn quote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn unquote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 { - 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 SassResult { +pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 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 { +pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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::() }; - 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 { +pub(crate) fn unique_id(args: ArgumentResult, _: &mut Visitor) -> SassResult { 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) { diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 7dd9b7df..5017c9d6 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -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}, + }; +} diff --git a/src/builtin/modules/list.rs b/src/builtin/modules/list.rs index 16c7a6c1..47cb92c5 100644 --- a/src/builtin/modules/list.rs +++ b/src/builtin/modules/list.rs @@ -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 { + 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); } diff --git a/src/builtin/modules/math.rs b/src/builtin/modules/math.rs index 838ee787..f2241cb3 100644 --- a/src/builtin/modules/math.rs +++ b/src/builtin/modules/math.rs @@ -1,30 +1,36 @@ -use std::cmp::Ordering; - -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::builtin_imports::*; + +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 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(); -fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult { + num * factor +} + +fn clamp(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { }; 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 { }; 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 { ), 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 { +fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult { 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> { + .map(|(idx, val)| -> SassResult { 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 { ) .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 { .into()) } }) - .collect::>>>()?; - - let rest = match rest { - Some(v) => v, - None => return Ok(Value::Dimension(None, first.1, true)), - }; + .collect::>>()?; 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 { +fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { ) .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 { } }; - 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 { ) .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 { } }; - 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 { +fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { ) .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 { }; 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 { ) .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 { } }; - 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 { +fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { ) .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 { } macro_rules! trig_fn { - ($name:ident, $name_deg:ident) => { - fn $name(mut args: CallArgs, _: &mut Parser) -> SassResult { + ($name:ident) => { + fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { +fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { ) .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 { }) } -fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult { +fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { ) .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 { }) } -fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult { +fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { ) .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 { }) } -fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult { +fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { 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 { }; 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 { }; 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 { ) .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 { .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, + }), ); } diff --git a/src/builtin/modules/meta.rs b/src/builtin/modules/meta.rs index f1037817..0d66175d 100644 --- a/src/builtin/modules/meta.rs +++ b/src/builtin/modules/meta.rs @@ -1,20 +1,20 @@ -use codemap::Spanned; - -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 std::cell::RefCell; +use std::collections::BTreeMap; +use std::sync::Arc; + +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> { +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> { } }; - 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` + let mut configuration = Configuration::empty(); + if let Some(with) = with { - let mut config = ModuleConfig::default(); + 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> { } }; - 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()); + } - let (_, stmts) = parser.load_module(&url, &mut config)?; + values.insert(name, ConfiguredValue::explicit(value, args.span())); + } - Ok(stmts) - } else { - parser.parse_single_import(&url, span) + 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 { +fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { +fn module_variables(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { 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 SassResult { + 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::>>()?; + + Ok(Value::List(args, ListSeparator::Comma, Brackets::None)) +} + +fn calc_name(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult { + 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); } diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs index 71465adb..2a200639 100644 --- a/src/builtin/modules/mod.rs +++ b/src/builtin/modules/mod.rs @@ -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>, + #[allow(dead_code)] + forward_rule: AstForwardRule, } -#[derive(Debug, Default)] -pub(crate) struct Modules(BTreeMap); - -#[derive(Debug, Default)] -pub(crate) struct ModuleConfig(BTreeMap); - -impl ModuleConfig { - /// Removes and returns element with name - pub fn get(&mut self, name: Identifier) -> Option { - self.0.remove(&name) +impl ForwardedModule { + pub fn if_necessary( + module: Arc>, + rule: AstForwardRule, + ) -> Arc> { + 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 { + Arc::new(RefCell::new(Module::Forwarded(ForwardedModule { + inner: module, + forward_rule: rule, + }))) + } } +} - /// 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() - } +#[derive(Debug, Clone)] +pub(crate) struct ModuleScope { + pub variables: Arc>, + pub mixins: Arc>, + pub functions: Arc>, +} - pub fn insert(&mut self, name: Spanned, value: Spanned) -> SassResult<()> { - if self.0.insert(name.node, value.node).is_some() { - Err(( - "The same variable may only be configured once.", - name.span.merge(value.span), - ) - .into()) - } else { - Ok(()) +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, + #[allow(dead_code)] + extension_store: ExtensionStore, + #[allow(dead_code)] + env: Environment, + }, + Builtin { + scope: ModuleScope, + }, + Forwarded(ForwardedModule), +} + +#[derive(Debug, Clone)] +pub(crate) struct Modules(BTreeMap>>); + 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>, + 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>> { 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>> { 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( + local: Arc>, + others: Vec>>, +) -> Arc> { + let local_map = PublicMemberMapView(local); + + if others.is_empty() { + return Arc::new(local_map); } + + let mut all_maps: Vec>> = + 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_env(env: Environment, extension_store: ExtensionStore) -> Self { + let variables = { + let variables = (*env.forwarded_modules).borrow(); + let variables = variables + .iter() + .map(|module| Arc::clone(&(*module).borrow().scope().variables)); + let this = Arc::new(BaseMapView(env.global_vars())); + member_map(this, variables.collect()) + }; + + let mixins = { + let mixins = (*env.forwarded_modules).borrow(); + let mixins = mixins + .iter() + .map(|module| Arc::clone(&(*module).borrow().scope().mixins)); + let this = Arc::new(BaseMapView(env.global_mixins())); + member_map(this, mixins.collect()) + }; + + let functions = { + let functions = (*env.forwarded_modules).borrow(); + let functions = functions + .iter() + .map(|module| Arc::clone(&(*module).borrow().scope().functions)); + let this = Arc::new(BaseMapView(env.global_functions())); + member_map(this, functions.collect()) + }; + + let scope = ModuleScope { + variables, + mixins, + functions, + }; + + Module::Environment { + scope, + upstream: Vec::new(), + extension_store, + env, + } + } + pub fn new_builtin() -> Self { - Module { - scope: Scope::default(), - modules: Modules::default(), - is_builtin: true, + Module::Builtin { + scope: ModuleScope::new(), } } - pub fn get_var(&self, name: Spanned) -> SassResult<&Value> { - if name.node.as_str().starts_with('-') { - return Err(( - "Private members can't be accessed from outside their modules.", - name.span, - ) - .into()); + fn scope(&self) -> ModuleScope { + match self { + Self::Builtin { scope } | Self::Environment { scope, .. } => scope.clone(), + Self::Forwarded(forwarded) => (*forwarded.inner).borrow().scope(), } + } - match self.scope.vars.get(&name.node) { + pub fn get_var(&self, name: Spanned) -> SassResult { + let scope = self.scope(); + + match scope.variables.get(name.node) { Some(v) => Ok(v), None => Err(("Undefined variable.", name.span).into()), } } - pub fn update_var(&mut self, name: Spanned, value: Value) -> SassResult<()> { - if self.is_builtin { - return Err(("Cannot modify built-in variable.", name.span).into()); - } + pub fn get_var_no_err(&self, name: Identifier) -> Option { + let scope = self.scope(); - if name.node.as_str().starts_with('-') { - return Err(( - "Private members can't be accessed from outside their modules.", - name.span, - ) - .into()); - } + scope.variables.get(name) + } - if self.scope.insert_var(name.node, value).is_some() { - Ok(()) - } else { - Err(("Undefined variable.", name.span).into()) + pub fn get_mixin_no_err(&self, name: Identifier) -> Option { + let scope = self.scope(); + + scope.mixins.get(name) + } + + pub fn update_var(&mut self, name: Spanned, value: Value) -> SassResult<()> { + 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()); } + + Ok(()) } pub fn get_mixin(&self, name: Spanned) -> SassResult { - 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) -> SassResult> { - 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 { + 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, + function: fn(ArgumentResult, &mut Visitor) -> SassResult, ) { 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::>(), + .collect::>(), ) } - 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::>(), + .collect::>(), ) } - - 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 { diff --git a/src/color/mod.rs b/src/color/mod.rs index d4bc53a7..649d848e 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -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, - 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); + + 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); - let repr = repr(&red, &green, &blue, &alpha); - Color::new_rgba(red, green, blue, alpha, repr) + 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 /// 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) } - /// 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_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, 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(); - } - }; - } - 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() - } - } + let m1 = scaled_lightness.mul_add(2.0, -m2); - 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 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); - let repr = repr(&red, &green, &blue, &alpha); - Color::new_hsla(red, green, blue, alpha, hsla, repr) + 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; + } + + 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 + } } 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; - - 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 factor = 1.0 - scaled_white - scaled_black; - 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 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) - } -} + 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); -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) } } diff --git a/src/common.rs b/src/common.rs index 2867ca1a..1e78a5af 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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 for Identifier { diff --git a/src/context_flags.rs b/src/context_flags.rs new file mode 100644 index 00000000..b3d3d7b7 --- /dev/null +++ b/src/context_flags.rs @@ -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 for u16 { + type Output = Self; + #[inline] + fn bitand(self, rhs: ContextFlag) -> Self::Output { + self & rhs.0 + } +} + +impl BitOr for ContextFlags { + type Output = Self; + fn bitor(self, rhs: ContextFlag) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for ContextFlags { + fn bitor_assign(&mut self, rhs: ContextFlag) { + self.0 |= rhs.0; + } +} diff --git a/src/error.rs b/src/error.rs index 17a947b3..1d78c673 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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::() )?; 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(()) } } diff --git a/src/evaluate/bin_op.rs b/src/evaluate/bin_op.rs new file mode 100644 index 00000000..ae2e0424 --- /dev/null +++ b/src/evaluate/bin_op.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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()) + } + }) +} diff --git a/src/evaluate/css_tree.rs b/src/evaluate/css_tree.rs new file mode 100644 index 00000000..e0208e8e --- /dev/null +++ b/src/evaluate/css_tree.rs @@ -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>>, + pub parent_to_child: BTreeMap>, + pub child_to_parent: BTreeMap, +} + +#[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> { + self.stmts[idx.0].borrow() + } + + pub fn get_mut(&self, idx: CssTreeIdx) -> RefMut> { + self.stmts[idx.0].borrow_mut() + } + + pub fn finish(self) -> Vec { + 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 { + 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 + } +} diff --git a/src/evaluate/env.rs b/src/evaluate/env.rs new file mode 100644 index 00000000..babfebc2 --- /dev/null +++ b/src/evaluate/env.rs @@ -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>, + pub global_modules: Vec>>, + pub content: Option>, + pub forwarded_modules: Arc>>>>, +} + +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>, 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, + namespace: Option>, + ) -> SassResult { + 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>, + ) -> SassResult> { + 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>, + ) -> SassResult { + 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, + namespace: Option>, + ) -> SassResult { + 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, + namespace: Option>, + 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>> { + self.scopes.global_variables() + } + + pub fn global_mixins(&self) -> Arc>> { + self.scopes.global_mixins() + } + + pub fn global_functions(&self) -> Arc>> { + self.scopes.global_functions() + } + + fn get_variable_from_global_modules(&self, name: Identifier) -> Option { + 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 { + 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 { + 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, + module: Arc>, + 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> { + debug_assert!(self.at_root()); + + Arc::new(RefCell::new(Module::new_env(self, extension_store))) + } +} diff --git a/src/evaluate/mod.rs b/src/evaluate/mod.rs new file mode 100644 index 00000000..4b428c73 --- /dev/null +++ b/src/evaluate/mod.rs @@ -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; diff --git a/src/evaluate/scope.rs b/src/evaluate/scope.rs new file mode 100644 index 00000000..578a4699 --- /dev/null +++ b/src/evaluate/scope.rs @@ -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>>>>>, + mixins: Arc>>>>>, + functions: Arc>>>>>, + len: Arc>, +} + +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>> { + debug_assert_eq!(self.len(), (*self.variables).borrow().len()); + Arc::clone(&(*self.variables).borrow()[0]) + } + + pub fn global_functions(&self) -> Arc>> { + Arc::clone(&(*self.functions).borrow()[0]) + } + + pub fn global_mixins(&self) -> Arc>> { + Arc::clone(&(*self.mixins).borrow()[0]) + } + + pub fn find_var(&self, name: Identifier) -> Option { + 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 { + 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 { + 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) -> SassResult { + 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) -> SassResult { + 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 { + 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()) + } +} diff --git a/src/evaluate/visitor.rs b/src/evaluate/visitor.rs new file mode 100644 index 00000000..eed10435 --- /dev/null +++ b/src/evaluate/visitor.rs @@ -0,0 +1,2878 @@ +use std::{ + cell::{Cell, RefCell}, + collections::{BTreeMap, BTreeSet, HashSet}, + ffi::OsStr, + fmt, + iter::FromIterator, + mem, + path::{Path, PathBuf}, + sync::Arc, +}; + +use codemap::{CodeMap, Span, Spanned}; +use indexmap::IndexSet; + +use crate::{ + ast::*, + builtin::{ + meta::if_arguments, + modules::{ + declare_module_color, declare_module_list, declare_module_map, declare_module_math, + declare_module_meta, declare_module_selector, declare_module_string, Module, + }, + GLOBAL_FUNCTIONS, + }, + common::{unvendor, BinaryOp, Identifier, ListSeparator, QuoteKind, UnaryOp}, + error::{SassError, SassResult}, + interner::InternedString, + lexer::Lexer, + parse::{ + AtRootQueryParser, CssParser, KeyframesSelectorParser, SassParser, ScssParser, + StylesheetParser, + }, + selector::{ + ComplexSelectorComponent, ExtendRule, ExtendedSelector, ExtensionStore, SelectorList, + SelectorParser, + }, + token::Token, + utils::{to_sentence, trim_ascii}, + value::{ + ArgList, CalculationArg, CalculationName, Number, SassCalculation, SassFunction, SassMap, + SassNumber, UserDefinedFunction, Value, + }, + ContextFlags, InputSyntax, Options, +}; + +use super::{ + bin_op::{add, cmp, div, mul, rem, single_eq, sub}, + css_tree::{CssTree, CssTreeIdx}, + env::Environment, +}; + +trait UserDefinedCallable { + fn name(&self) -> Identifier; + fn arguments(&self) -> &ArgumentDeclaration; +} + +impl UserDefinedCallable for AstFunctionDecl { + fn name(&self) -> Identifier { + self.name.node + } + + fn arguments(&self) -> &ArgumentDeclaration { + &self.arguments + } +} + +impl UserDefinedCallable for AstMixin { + fn name(&self) -> Identifier { + self.name + } + + fn arguments(&self) -> &ArgumentDeclaration { + &self.args + } +} + +impl UserDefinedCallable for Arc { + fn name(&self) -> Identifier { + Identifier::from("@content") + } + + fn arguments(&self) -> &ArgumentDeclaration { + &self.content.args + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CallableContentBlock { + content: AstContentBlock, + env: Environment, +} + +pub(crate) struct Visitor<'a> { + pub declaration_name: Option, + pub flags: ContextFlags, + pub env: Environment, + pub style_rule_ignoring_at_root: Option, + // avoid emitting duplicate warnings for the same span + pub warnings_emitted: HashSet, + pub media_queries: Option>, + pub media_query_sources: Option>, + pub extender: ExtensionStore, + pub current_import_path: PathBuf, + pub is_plain_css: bool, + css_tree: CssTree, + parent: Option, + configuration: Arc>, + import_nodes: Vec, + pub options: &'a Options<'a>, + pub map: &'a mut CodeMap, + // todo: remove + span_before: Span, +} + +impl<'a> Visitor<'a> { + pub fn new( + path: &Path, + options: &'a Options<'a>, + map: &'a mut CodeMap, + span_before: Span, + ) -> Self { + let mut flags = ContextFlags::empty(); + flags.set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, true); + + let extender = ExtensionStore::new(span_before); + + let current_import_path = path.to_path_buf(); + + Self { + declaration_name: None, + style_rule_ignoring_at_root: None, + flags, + warnings_emitted: HashSet::new(), + media_queries: None, + media_query_sources: None, + env: Environment::new(), + extender, + css_tree: CssTree::new(), + parent: None, + current_import_path, + configuration: Arc::new(RefCell::new(Configuration::empty())), + is_plain_css: false, + import_nodes: Vec::new(), + options, + span_before, + map, + } + } + + pub fn visit_stylesheet(&mut self, mut style_sheet: StyleSheet) -> SassResult<()> { + let was_in_plain_css = self.is_plain_css; + self.is_plain_css = style_sheet.is_plain_css; + mem::swap(&mut self.current_import_path, &mut style_sheet.url); + + for stmt in style_sheet.body { + let result = self.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + mem::swap(&mut self.current_import_path, &mut style_sheet.url); + self.is_plain_css = was_in_plain_css; + + Ok(()) + } + + pub fn finish(mut self) -> Vec { + let mut finished_tree = self.css_tree.finish(); + if self.import_nodes.is_empty() { + finished_tree + } else { + self.import_nodes.append(&mut finished_tree); + self.import_nodes + } + } + + fn visit_return_rule(&mut self, ret: AstReturn) -> SassResult> { + let val = self.visit_expr(ret.val)?; + + Ok(Some(self.without_slash(val))) + } + + // todo: we really don't have to return Option from all of these children + pub fn visit_stmt(&mut self, stmt: AstStmt) -> SassResult> { + match stmt { + AstStmt::RuleSet(ruleset) => self.visit_ruleset(ruleset), + AstStmt::Style(style) => self.visit_style(style), + AstStmt::SilentComment(..) => Ok(None), + AstStmt::If(if_stmt) => self.visit_if_stmt(if_stmt), + AstStmt::For(for_stmt) => self.visit_for_stmt(for_stmt), + AstStmt::Return(ret) => self.visit_return_rule(ret), + AstStmt::Each(each_stmt) => self.visit_each_stmt(each_stmt), + AstStmt::Media(media_rule) => self.visit_media_rule(media_rule), + AstStmt::Include(include_stmt) => self.visit_include_stmt(include_stmt), + AstStmt::While(while_stmt) => self.visit_while_stmt(&while_stmt), + AstStmt::VariableDecl(decl) => self.visit_variable_decl(decl), + AstStmt::LoudComment(comment) => self.visit_loud_comment(comment), + AstStmt::ImportRule(import_rule) => self.visit_import_rule(import_rule), + AstStmt::FunctionDecl(func) => { + self.visit_function_decl(func); + Ok(None) + } + AstStmt::Mixin(mixin) => { + self.visit_mixin_decl(mixin); + Ok(None) + } + AstStmt::ContentRule(content_rule) => self.visit_content_rule(content_rule), + AstStmt::Warn(warn_rule) => { + self.visit_warn_rule(warn_rule)?; + Ok(None) + } + AstStmt::UnknownAtRule(unknown_at_rule) => self.visit_unknown_at_rule(unknown_at_rule), + AstStmt::ErrorRule(error_rule) => Err(self.visit_error_rule(error_rule)?), + AstStmt::Extend(extend_rule) => self.visit_extend_rule(extend_rule), + AstStmt::AtRootRule(at_root_rule) => self.visit_at_root_rule(at_root_rule), + AstStmt::Debug(debug_rule) => self.visit_debug_rule(debug_rule), + AstStmt::Use(use_rule) => { + self.visit_use_rule(use_rule)?; + Ok(None) + } + AstStmt::Forward(forward_rule) => { + self.visit_forward_rule(forward_rule)?; + Ok(None) + } + AstStmt::Supports(supports_rule) => { + self.visit_supports_rule(supports_rule)?; + Ok(None) + } + } + } + + fn visit_forward_rule(&mut self, forward_rule: AstForwardRule) -> SassResult<()> { + let old_config = Arc::clone(&self.configuration); + let adjusted_config = + Configuration::through_forward(Arc::clone(&old_config), &forward_rule); + + if !forward_rule.configuration.is_empty() { + let new_configuration = + self.add_forward_configuration(Arc::clone(&adjusted_config), &forward_rule)?; + + self.load_module( + forward_rule.url.as_path(), + Some(Arc::clone(&new_configuration)), + false, + forward_rule.span, + |visitor, module, _| { + visitor.env.forward_module(module, forward_rule.clone()); + + Ok(()) + }, + )?; + + Self::remove_used_configuration( + &adjusted_config, + &new_configuration, + &forward_rule + .configuration + .iter() + .filter(|var| !var.is_guarded) + .map(|var| var.name.node) + .collect(), + ); + + // Remove all the variables that weren't configured by this particular + // `@forward` before checking that the configuration is empty. Errors for + // outer `with` clauses will be thrown once those clauses finish + // executing. + let configured_variables: HashSet = forward_rule + .configuration + .iter() + .map(|var| var.name.node) + .collect(); + + let mut to_remove = Vec::new(); + + for name in (*new_configuration).borrow().values.keys() { + if !configured_variables.contains(&name) { + to_remove.push(name); + } + } + + for name in to_remove { + (*new_configuration).borrow_mut().remove(name); + } + + Self::assert_configuration_is_empty(&new_configuration, false)?; + } else { + self.configuration = adjusted_config; + let url = forward_rule.url.clone(); + self.load_module( + url.as_path(), + None, + false, + forward_rule.span, + move |visitor, module, _| { + visitor.env.forward_module(module, forward_rule.clone()); + + Ok(()) + }, + )?; + self.configuration = old_config; + } + + Ok(()) + } + + #[allow(clippy::unnecessary_unwrap)] + fn add_forward_configuration( + &mut self, + config: Arc>, + forward_rule: &AstForwardRule, + ) -> SassResult>> { + let mut new_values = BTreeMap::from_iter((*config).borrow().values.iter().into_iter()); + + for variable in &forward_rule.configuration { + if variable.is_guarded { + let old_value = (*config).borrow_mut().remove(variable.name.node); + + if old_value.is_some() + && !matches!( + old_value, + Some(ConfiguredValue { + value: Value::Null, + .. + }) + ) + { + new_values.insert(variable.name.node, old_value.unwrap()); + continue; + } + } + + // todo: superfluous clone? + let value = self.visit_expr(variable.expr.node.clone())?; + let value = self.without_slash(value); + + new_values.insert( + variable.name.node, + ConfiguredValue::explicit(value, variable.expr.span), + ); + } + + Ok(Arc::new(RefCell::new( + if !(*config).borrow().is_implicit() || (*config).borrow().is_empty() { + Configuration::explicit(new_values, forward_rule.span) + } else { + Configuration::implicit(new_values) + }, + ))) + } + + fn remove_used_configuration( + upstream: &Arc>, + downstream: &Arc>, + except: &HashSet, + ) { + let mut names_to_remove = Vec::new(); + let downstream_keys = (*downstream).borrow().values.keys(); + for name in (*upstream).borrow().values.keys() { + if except.contains(&name) { + continue; + } + + if !downstream_keys.contains(&name) { + names_to_remove.push(name); + } + } + + for name in names_to_remove { + (*upstream).borrow_mut().remove(name); + } + } + + fn parenthesize_supports_condition( + &mut self, + condition: AstSupportsCondition, + operator: Option<&str>, + ) -> SassResult { + match &condition { + AstSupportsCondition::Negation(..) => { + Ok(format!("({})", self.visit_supports_condition(condition)?)) + } + AstSupportsCondition::Operation { + operator: operator2, + .. + } if operator2.is_none() || operator2.as_deref() != operator => { + Ok(format!("({})", self.visit_supports_condition(condition)?)) + } + _ => self.visit_supports_condition(condition), + } + } + + fn visit_supports_condition(&mut self, condition: AstSupportsCondition) -> SassResult { + match condition { + AstSupportsCondition::Operation { + left, + operator, + right, + } => Ok(format!( + "{} {} {}", + self.parenthesize_supports_condition(*left, operator.as_deref())?, + operator.as_ref().unwrap(), + self.parenthesize_supports_condition(*right, operator.as_deref())? + )), + AstSupportsCondition::Negation(condition) => Ok(format!( + "not {}", + self.parenthesize_supports_condition(*condition, None)? + )), + AstSupportsCondition::Interpolation(expr) => { + self.evaluate_to_css(expr, QuoteKind::None, self.span_before) + } + AstSupportsCondition::Declaration { name, value } => { + let old_in_supports_decl = self.flags.in_supports_declaration(); + self.flags.set(ContextFlags::IN_SUPPORTS_DECLARATION, true); + + let is_custom_property = match &name { + AstExpr::String(StringExpr(text, QuoteKind::None), ..) => { + text.initial_plain().starts_with("--") + } + _ => false, + }; + + let result = format!( + "({}:{}{})", + self.evaluate_to_css(name, QuoteKind::Quoted, self.span_before)?, + if is_custom_property { "" } else { " " }, + self.evaluate_to_css(value, QuoteKind::Quoted, self.span_before)?, + ); + + self.flags + .set(ContextFlags::IN_SUPPORTS_DECLARATION, old_in_supports_decl); + + Ok(result) + } + AstSupportsCondition::Function { name, args } => Ok(format!( + "{}({})", + self.perform_interpolation(name, false)?, + self.perform_interpolation(args, false)? + )), + AstSupportsCondition::Anything { contents } => Ok(format!( + "({})", + self.perform_interpolation(contents, false)?, + )), + } + } + + fn visit_supports_rule(&mut self, supports_rule: AstSupportsRule) -> SassResult<()> { + if self.declaration_name.is_some() { + return Err(( + "Supports rules may not be used within nested declarations.", + supports_rule.span, + ) + .into()); + } + + let condition = self.visit_supports_condition(supports_rule.condition)?; + + let css_supports_rule = CssStmt::Supports( + SupportsRule { + params: condition, + body: Vec::new(), + }, + false, + ); + + let children = supports_rule.children; + + self.with_parent::>( + css_supports_rule, + true, + |visitor| { + if !visitor.style_rule_exists() { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + } else { + // If we're in a style rule, copy it into the supports rule so that + // declarations immediately inside @supports have somewhere to go. + // + // For example, "a {@supports (a: b) {b: c}}" should produce "@supports + // (a: b) {a {b: c}}". + let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); + let ruleset = CssStmt::RuleSet { + selector, + body: Vec::new(), + is_group_end: false, + }; + + visitor.with_parent::>( + ruleset, + false, + |visitor| { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + }, + |_| false, + )?; + } + + Ok(()) + }, + CssStmt::is_style_rule, + )?; + + Ok(()) + } + + fn execute( + &mut self, + stylesheet: StyleSheet, + configuration: Option>>, + // todo: different errors based on this + _names_in_errors: bool, + ) -> SassResult>> { + let env = Environment::new(); + let mut extension_store = ExtensionStore::new(self.span_before); + + self.with_environment::>(env.new_closure(), |visitor| { + let old_parent = visitor.parent; + mem::swap(&mut visitor.extender, &mut extension_store); + let old_style_rule = visitor.style_rule_ignoring_at_root.take(); + let old_media_queries = visitor.media_queries.take(); + let old_declaration_name = visitor.declaration_name.take(); + let old_in_unknown_at_rule = visitor.flags.in_unknown_at_rule(); + let old_at_root_excluding_style_rule = visitor.flags.at_root_excluding_style_rule(); + let old_in_keyframes = visitor.flags.in_keyframes(); + let old_configuration = if let Some(new_config) = configuration { + Some(mem::replace(&mut visitor.configuration, new_config)) + } else { + None + }; + visitor.parent = None; + visitor.flags.set(ContextFlags::IN_UNKNOWN_AT_RULE, false); + visitor + .flags + .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, false); + visitor.flags.set(ContextFlags::IN_KEYFRAMES, false); + + visitor.visit_stylesheet(stylesheet)?; + + // visitor.importer = old_importer; + // visitor.stylesheet = old_stylesheet; + // visitor.root = old_root; + visitor.parent = old_parent; + // visitor.end_of_imports = old_end_of_imports; + // visitor.out_of_order_imports = old_out_of_order_imports; + mem::swap(&mut visitor.extender, &mut extension_store); + visitor.style_rule_ignoring_at_root = old_style_rule; + visitor.media_queries = old_media_queries; + visitor.declaration_name = old_declaration_name; + visitor + .flags + .set(ContextFlags::IN_UNKNOWN_AT_RULE, old_in_unknown_at_rule); + visitor.flags.set( + ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, + old_at_root_excluding_style_rule, + ); + visitor + .flags + .set(ContextFlags::IN_KEYFRAMES, old_in_keyframes); + if let Some(old_config) = old_configuration { + visitor.configuration = old_config; + } + + Ok(()) + })?; + + let module = env.to_module(extension_store); + + Ok(module) + } + + pub fn load_module( + &mut self, + url: &Path, + configuration: Option>>, + names_in_errors: bool, + span: Span, + callback: impl Fn(&mut Self, Arc>, StyleSheet) -> SassResult<()>, + ) -> SassResult<()> { + let builtin = match url.to_string_lossy().as_ref() { + "sass:color" => Some(declare_module_color()), + "sass:list" => Some(declare_module_list()), + "sass:map" => Some(declare_module_map()), + "sass:math" => Some(declare_module_math()), + "sass:meta" => Some(declare_module_meta()), + "sass:selector" => Some(declare_module_selector()), + "sass:string" => Some(declare_module_string()), + _ => None, + }; + + if let Some(builtin) = builtin { + // todo: lots of ugly unwraps here + if configuration.is_some() + && !(**configuration.as_ref().unwrap()).borrow().is_implicit() + { + let msg = if names_in_errors { + format!( + "Built-in module {} can't be configured.", + url.to_string_lossy() + ) + } else { + "Built-in modules can't be configured.".to_owned() + }; + + return Err(( + msg, + (**configuration.as_ref().unwrap()).borrow().span.unwrap(), + ) + .into()); + } + + callback( + self, + Arc::new(RefCell::new(builtin)), + StyleSheet::new(false, PathBuf::from("")), + )?; + return Ok(()); + } + + // todo: decide on naming convention for style_sheet vs stylesheet + let stylesheet = self.load_style_sheet(url.to_string_lossy().as_ref(), false, span)?; + + let module = self.execute(stylesheet.clone(), configuration, names_in_errors)?; + + callback(self, module, stylesheet)?; + + Ok(()) + } + + fn visit_use_rule(&mut self, use_rule: AstUseRule) -> SassResult<()> { + let configuration = if use_rule.configuration.is_empty() { + Arc::new(RefCell::new(Configuration::empty())) + } else { + let mut values = BTreeMap::new(); + + for var in use_rule.configuration { + let value = self.visit_expr(var.expr.node)?; + let value = self.without_slash(value); + values.insert( + var.name.node, + ConfiguredValue::explicit(value, var.name.span.merge(var.expr.span)), + ); + } + + Arc::new(RefCell::new(Configuration::explicit(values, use_rule.span))) + }; + + let span = use_rule.span; + + let namespace = use_rule + .namespace + .as_ref() + .map(|s| Identifier::from(s.trim_start_matches("sass:"))); + + self.load_module( + &use_rule.url, + Some(Arc::clone(&configuration)), + false, + span, + |visitor, module, _| { + visitor.env.add_module(namespace, module, span)?; + + Ok(()) + }, + )?; + + Self::assert_configuration_is_empty(&configuration, false)?; + + Ok(()) + } + + pub fn assert_configuration_is_empty( + config: &Arc>, + name_in_error: bool, + ) -> SassResult<()> { + let config = (**config).borrow(); + // By definition, implicit configurations are allowed to only use a subset + // of their values. + if config.is_empty() || config.is_implicit() { + return Ok(()); + } + + let Spanned { node: name, span } = config.first().unwrap(); + + let msg = if name_in_error { + format!("${name} was not declared with !default in the @used module.") + } else { + "This variable was not declared with !default in the @used module.".to_owned() + }; + + Err((msg, span).into()) + } + + fn visit_import_rule(&mut self, import_rule: AstImportRule) -> SassResult> { + for import in import_rule.imports { + match import { + AstImport::Sass(dynamic_import) => { + self.visit_dynamic_import_rule(&dynamic_import)?; + } + AstImport::Plain(static_import) => self.visit_static_import_rule(static_import)?, + } + } + + Ok(None) + } + + /// Searches the current directory of the file then searches in `load_paths` directories + /// if the import has not yet been found. + /// + /// + /// + #[allow(clippy::cognitive_complexity)] + fn find_import(&self, path: &Path) -> Option { + let path_buf = if path.is_absolute() { + path.into() + } else { + self.current_import_path + .parent() + .unwrap_or_else(|| Path::new("")) + .join(path) + }; + + macro_rules! try_path { + ($path:expr) => { + let path = $path; + let dirname = path.parent().unwrap_or_else(|| Path::new("")); + let basename = path.file_name().unwrap_or_else(|| OsStr::new("..")); + + let partial = dirname.join(format!("_{}", basename.to_str().unwrap())); + + if self.options.fs.is_file(&path) { + return Some(path.to_path_buf()); + } + + if self.options.fs.is_file(&partial) { + return Some(partial); + } + }; + } + + if path_buf.extension() == Some(OsStr::new("scss")) + || path_buf.extension() == Some(OsStr::new("sass")) + || path_buf.extension() == Some(OsStr::new("css")) + { + let extension = path_buf.extension().unwrap(); + try_path!(path.with_extension(format!(".import{}", extension.to_str().unwrap()))); + try_path!(path); + return None; + } + + macro_rules! try_path_with_extensions { + ($path:expr) => { + let path = $path; + try_path!(path.with_extension("import.sass")); + try_path!(path.with_extension("import.scss")); + try_path!(path.with_extension("import.css")); + try_path!(path.with_extension("sass")); + try_path!(path.with_extension("scss")); + try_path!(path.with_extension("css")); + }; + } + + try_path_with_extensions!(path_buf.clone()); + + if self.options.fs.is_dir(&path_buf) { + try_path_with_extensions!(path_buf.join("index")); + } + + for load_path in &self.options.load_paths { + let path_buf = load_path.join(path); + + try_path_with_extensions!(&path_buf); + + if self.options.fs.is_dir(&path_buf) { + try_path_with_extensions!(path_buf.join("index")); + } + } + + None + } + + fn parse_file( + &mut self, + lexer: Lexer, + path: &Path, + span_before: Span, + ) -> SassResult { + match InputSyntax::for_path(path) { + InputSyntax::Scss => { + ScssParser::new(lexer, self.map, self.options, span_before, path).__parse() + } + InputSyntax::Sass => { + SassParser::new(lexer, self.map, self.options, span_before, path).__parse() + } + InputSyntax::Css => { + CssParser::new(lexer, self.map, self.options, span_before, path).__parse() + } + } + } + + fn import_like_node( + &mut self, + url: &str, + _for_import: bool, + span: Span, + ) -> SassResult { + if let Some(name) = self.find_import(url.as_ref()) { + let file = self.map.add_file( + name.to_string_lossy().into(), + String::from_utf8(self.options.fs.read(&name)?)?, + ); + + let old_is_use_allowed = self.flags.is_use_allowed(); + self.flags.set(ContextFlags::IS_USE_ALLOWED, true); + + let style_sheet = + self.parse_file(Lexer::new_from_file(&file), &name, file.span.subspan(0, 0))?; + + self.flags + .set(ContextFlags::IS_USE_ALLOWED, old_is_use_allowed); + return Ok(style_sheet); + } + + Err(("Can't find stylesheet to import.", span).into()) + } + + pub fn load_style_sheet( + &mut self, + url: &str, + // default=false + for_import: bool, + span: Span, + ) -> SassResult { + // todo: import cache + self.import_like_node(url, for_import, span) + } + + fn visit_dynamic_import_rule(&mut self, dynamic_import: &AstSassImport) -> SassResult<()> { + let stylesheet = self.load_style_sheet(&dynamic_import.url, true, dynamic_import.span)?; + + // If the imported stylesheet doesn't use any modules, we can inject its + // CSS directly into the current stylesheet. If it does use modules, we + // need to put its CSS into an intermediate [ModifiableCssStylesheet] so + // that we can hermetically resolve `@extend`s before injecting it. + if stylesheet.uses.is_empty() && stylesheet.forwards.is_empty() { + self.visit_stylesheet(stylesheet)?; + return Ok(()); + } + + // this todo should be unreachable, as we currently do not push + // to stylesheet.uses or stylesheet.forwards + todo!() + } + + fn visit_static_import_rule(&mut self, static_import: AstPlainCssImport) -> SassResult<()> { + let import = self.interpolation_to_value(static_import.url, false, false)?; + + let modifiers = static_import + .modifiers + .map(|modifiers| self.interpolation_to_value(modifiers, false, false)) + .transpose()?; + + let node = CssStmt::Import(import, modifiers); + + if self.parent.is_some() && self.parent != Some(CssTree::ROOT) { + self.css_tree.add_stmt(node, self.parent); + } else { + self.import_nodes.push(node); + } + + Ok(()) + } + + fn visit_debug_rule(&mut self, debug_rule: AstDebugRule) -> SassResult> { + if self.options.quiet { + return Ok(None); + } + + let message = self.visit_expr(debug_rule.value)?; + + let loc = self.map.look_up_span(debug_rule.span); + eprintln!( + "{}:{} DEBUG: {}", + loc.file.name(), + loc.begin.line + 1, + message.inspect(debug_rule.span)? + ); + + Ok(None) + } + + fn visit_content_rule(&mut self, content_rule: AstContentRule) -> SassResult> { + let span = content_rule.args.span; + if let Some(content) = &self.env.content { + self.run_user_defined_callable( + MaybeEvaledArguments::Invocation(content_rule.args), + Arc::clone(content), + &content.env.clone(), + span, + |content, visitor| { + for stmt in content.content.body.clone() { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + }, + )?; + } + + Ok(None) + } + + fn trim_included(&self, nodes: &[CssTreeIdx]) -> CssTreeIdx { + if nodes.is_empty() { + return CssTree::ROOT; + } + + let mut parent = self.parent; + + let mut innermost_contiguous: Option = None; + + for i in 0..nodes.len() { + while parent != nodes.get(i).copied() { + innermost_contiguous = None; + + let grandparent = self.css_tree.child_to_parent.get(&parent.unwrap()).copied(); + if grandparent.is_none() { + unreachable!( + "Expected {:?} to be an ancestor of {:?}.", + nodes[i], grandparent + ) + } + parent = grandparent; + } + innermost_contiguous = innermost_contiguous.or(Some(i)); + + let grandparent = self.css_tree.child_to_parent.get(&parent.unwrap()).copied(); + if grandparent.is_none() { + unreachable!( + "Expected {:?} to be an ancestor of {:?}.", + nodes[i], grandparent + ) + } + parent = grandparent; + } + + if parent != Some(CssTree::ROOT) { + return CssTree::ROOT; + } + + nodes[innermost_contiguous.unwrap()] + } + + fn visit_at_root_rule(&mut self, mut at_root_rule: AstAtRootRule) -> SassResult> { + let query = match at_root_rule.query.clone() { + Some(val) => { + let resolved = self.perform_interpolation(val, true)?; + + let query_toks = Lexer::new( + resolved + .chars() + .map(|x| Token::new(self.span_before, x)) + .collect(), + ); + + AtRootQueryParser::new(query_toks).parse()? + } + None => AtRootQuery::default(), + }; + + let mut current_parent_idx = self.parent; + + let mut included = Vec::new(); + + while let Some(parent_idx) = current_parent_idx { + let parent = self.css_tree.get(parent_idx); + let grandparent_idx = match &*parent { + Some(parent) => { + if !query.excludes(parent) { + included.push(parent_idx); + } + self.css_tree.child_to_parent.get(&parent_idx).copied() + } + None => break, + }; + + current_parent_idx = grandparent_idx; + } + + let root = self.trim_included(&included); + + // If we didn't exclude any rules, we don't need to use the copies we might + // have created. + if Some(root) == self.parent { + self.with_scope::>(false, true, |visitor| { + for stmt in at_root_rule.children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + })?; + return Ok(None); + } + + let inner_copy = if !included.is_empty() { + let inner_copy = self + .css_tree + .get(*included.first().unwrap()) + .as_ref() + .map(CssStmt::copy_without_children); + let mut outer_copy = self.css_tree.add_stmt(inner_copy.unwrap(), None); + + for node in &included[1..] { + let copy = self + .css_tree + .get(*node) + .as_ref() + .map(CssStmt::copy_without_children) + .unwrap(); + + let copy_idx = self.css_tree.add_stmt(copy, None); + self.css_tree.link_child_to_parent(outer_copy, copy_idx); + + outer_copy = copy_idx; + } + + Some(outer_copy) + } else { + let inner_copy = self + .css_tree + .get(root) + .as_ref() + .map(CssStmt::copy_without_children); + inner_copy.map(|p| self.css_tree.add_stmt(p, None)) + }; + + let body = mem::take(&mut at_root_rule.children); + + self.with_scope_for_at_root::>(inner_copy, &query, |visitor| { + for stmt in body { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + })?; + + Ok(None) + } + + fn with_scope_for_at_root( + &mut self, + new_parent_idx: Option, + query: &AtRootQuery, + callback: impl FnOnce(&mut Self) -> T, + ) -> T { + let old_parent = self.parent; + self.parent = new_parent_idx; + + let old_at_root_excluding_style_rule = self.flags.at_root_excluding_style_rule(); + + if query.excludes_style_rules() { + self.flags + .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, true); + } + + let old_media_query_info = if self.media_queries.is_some() && query.excludes_name("media") { + Some((self.media_queries.take(), self.media_query_sources.take())) + } else { + None + }; + + let was_in_keyframes = if self.flags.in_keyframes() && query.excludes_name("keyframes") { + let was = self.flags.in_keyframes(); + self.flags.set(ContextFlags::IN_KEYFRAMES, false); + was + } else { + self.flags.in_keyframes() + }; + + // todo: + // if self.flags.in_unknown_at_rule() && !included.iter().any(|parent| parent is CssAtRule) + + let res = self.with_scope(false, true, callback); + + self.parent = old_parent; + + self.flags.set( + ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, + old_at_root_excluding_style_rule, + ); + + if let Some((old_media_queries, old_media_query_sources)) = old_media_query_info { + self.media_queries = old_media_queries; + self.media_query_sources = old_media_query_sources; + } + + self.flags.set(ContextFlags::IN_KEYFRAMES, was_in_keyframes); + + res + } + + fn visit_function_decl(&mut self, fn_decl: AstFunctionDecl) { + let name = fn_decl.name.node; + // todo: independency + + let func = SassFunction::UserDefined(UserDefinedFunction { + function: Box::new(fn_decl), + name, + env: self.env.new_closure(), + }); + + self.env.insert_fn(func); + } + + pub fn parse_selector_from_string( + &mut self, + selector_text: &str, + allows_parent: bool, + allows_placeholder: bool, + span: Span, + ) -> SassResult { + let sel_toks = Lexer::new(selector_text.chars().map(|x| Token::new(span, x)).collect()); + + SelectorParser::new(sel_toks, allows_parent, allows_placeholder, span).parse() + } + + fn visit_extend_rule(&mut self, extend_rule: AstExtendRule) -> SassResult> { + if !self.style_rule_exists() || self.declaration_name.is_some() { + return Err(( + "@extend may only be used within style rules.", + extend_rule.span, + ) + .into()); + } + + let super_selector = self.style_rule_ignoring_at_root.clone().unwrap(); + + let target_text = self.interpolation_to_value(extend_rule.value, false, true)?; + + let list = self.parse_selector_from_string(&target_text, false, true, extend_rule.span)?; + + for complex in list.components { + if complex.components.len() != 1 || !complex.components.first().unwrap().is_compound() { + // If the selector was a compound selector but not a simple + // selector, emit a more explicit error. + return Err(("complex selectors may not be extended.", extend_rule.span).into()); + } + + let compound = match complex.components.first() { + Some(ComplexSelectorComponent::Compound(c)) => c, + Some(..) | None => todo!(), + }; + if compound.components.len() != 1 { + return Err(( + format!( + "compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n", + compound.components.iter().map(ToString::to_string).collect::>().join(", ") + ) + , extend_rule.span).into()); + } + + self.extender.add_extension( + super_selector.clone().into_selector().0, + compound.components.first().unwrap(), + &ExtendRule { + is_optional: extend_rule.is_optional, + }, + &self.media_queries, + extend_rule.span, + ); + } + + Ok(None) + } + + fn visit_error_rule(&mut self, error_rule: AstErrorRule) -> SassResult> { + let value = self + .visit_expr(error_rule.value)? + .inspect(error_rule.span)? + .into_owned(); + + Ok((value, error_rule.span).into()) + } + + fn merge_media_queries( + queries1: &[MediaQuery], + queries2: &[MediaQuery], + ) -> Option> { + let mut queries = Vec::new(); + + for query1 in queries1 { + for query2 in queries2 { + match query1.merge(query2) { + MediaQueryMergeResult::Empty => continue, + MediaQueryMergeResult::Unrepresentable => return None, + MediaQueryMergeResult::Success(result) => queries.push(result), + } + } + } + + Some(queries) + } + + fn visit_media_queries(&mut self, queries: Interpolation) -> SassResult> { + let resolved = self.perform_interpolation(queries, true)?; + + CssMediaQuery::parse_list(&resolved, self.span_before) + } + + fn visit_media_rule(&mut self, media_rule: AstMedia) -> SassResult> { + if self.declaration_name.is_some() { + return Err(( + "Media rules may not be used within nested declarations.", + media_rule.span, + ) + .into()); + } + + let queries1 = self.visit_media_queries(media_rule.query)?; + // todo: superfluous clone? + let queries2 = self.media_queries.clone(); + let merged_queries = queries2 + .as_ref() + .and_then(|queries2| Self::merge_media_queries(queries2, &queries1)); + + let merged_sources = match &merged_queries { + Some(merged_queries) if merged_queries.is_empty() => return Ok(None), + Some(..) => { + let mut set = IndexSet::new(); + set.extend(self.media_query_sources.clone().unwrap().into_iter()); + set.extend(self.media_queries.clone().unwrap().into_iter()); + set.extend(queries1.clone().into_iter()); + set + } + None => IndexSet::new(), + }; + + let children = media_rule.body; + + let query = merged_queries.clone().unwrap_or_else(|| queries1.clone()); + + let media_rule = CssStmt::Media( + MediaRule { + query, + body: Vec::new(), + }, + false, + ); + + self.with_parent::>( + media_rule, + true, + |visitor| { + visitor.with_media_queries( + Some(merged_queries.unwrap_or(queries1)), + Some(merged_sources.clone()), + |visitor| { + if !visitor.style_rule_exists() { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + } else { + // If we're in a style rule, copy it into the media query so that + // declarations immediately inside @media have somewhere to go. + // + // For example, "a {@media screen {b: c}}" should produce + // "@media screen {a {b: c}}". + let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); + let ruleset = CssStmt::RuleSet { + selector, + body: Vec::new(), + is_group_end: false, + }; + + visitor.with_parent::>( + ruleset, + false, + |visitor| { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + }, + |_| false, + )?; + } + + Ok(()) + }, + ) + }, + |stmt| match stmt { + CssStmt::RuleSet { .. } => true, + // todo: node.queries.every(mergedSources.contains)) + CssStmt::Media(media_rule, ..) => { + !merged_sources.is_empty() + && media_rule + .query + .iter() + .all(|query| merged_sources.contains(query)) + } + _ => false, + }, + )?; + + Ok(None) + } + + fn visit_unknown_at_rule( + &mut self, + unknown_at_rule: AstUnknownAtRule, + ) -> SassResult> { + if self.declaration_name.is_some() { + return Err(( + "At-rules may not be used within nested declarations.", + unknown_at_rule.span, + ) + .into()); + } + + let name = self.interpolation_to_value(unknown_at_rule.name, false, false)?; + + let value = unknown_at_rule + .value + .map(|v| self.interpolation_to_value(v, true, true)) + .transpose()?; + + if unknown_at_rule.children.is_none() { + let stmt = CssStmt::UnknownAtRule( + UnknownAtRule { + name, + params: value.unwrap_or_default(), + body: Vec::new(), + has_body: false, + }, + false, + ); + + self.css_tree.add_stmt(stmt, self.parent); + + return Ok(None); + } + + let was_in_keyframes = self.flags.in_keyframes(); + let was_in_unknown_at_rule = self.flags.in_unknown_at_rule(); + + if unvendor(&name) == "keyframes" { + self.flags.set(ContextFlags::IN_KEYFRAMES, true); + } else { + self.flags.set(ContextFlags::IN_UNKNOWN_AT_RULE, true); + } + + let children = unknown_at_rule.children.unwrap(); + + let stmt = CssStmt::UnknownAtRule( + UnknownAtRule { + name, + params: value.unwrap_or_default(), + body: Vec::new(), + has_body: true, + }, + false, + ); + + self.with_parent::>( + stmt, + true, + |visitor| { + if !visitor.style_rule_exists() || visitor.flags.in_keyframes() { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + } else { + // If we're in a style rule, copy it into the at-rule so that + // declarations immediately inside it have somewhere to go. + // + // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}". + let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); + + let style_rule = CssStmt::RuleSet { + selector, + body: Vec::new(), + is_group_end: false, + }; + + visitor.with_parent::>( + style_rule, + false, + |visitor| { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + }, + |_| false, + )?; + } + + Ok(()) + }, + CssStmt::is_style_rule, + )?; + + self.flags.set(ContextFlags::IN_KEYFRAMES, was_in_keyframes); + self.flags + .set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule); + + Ok(None) + } + + pub fn emit_warning(&mut self, message: &str, span: Span) { + if self.options.quiet { + return; + } + let loc = self.map.look_up_span(span); + eprintln!( + "Warning: {}\n ./{}:{}:{}", + message, + loc.file.name(), + loc.begin.line + 1, + loc.begin.column + 1 + ); + } + + fn visit_warn_rule(&mut self, warn_rule: AstWarn) -> SassResult<()> { + if self.warnings_emitted.insert(warn_rule.span) { + let value = self.visit_expr(warn_rule.value)?; + let message = value.to_css_string(warn_rule.span, self.options.is_compressed())?; + self.emit_warning(&message, warn_rule.span); + } + + Ok(()) + } + + fn with_media_queries( + &mut self, + queries: Option>, + sources: Option>, + callback: impl FnOnce(&mut Self) -> T, + ) -> T { + let old_media_queries = self.media_queries.take(); + let old_media_query_sources = self.media_query_sources.take(); + self.media_queries = queries; + self.media_query_sources = sources; + let result = callback(self); + self.media_queries = old_media_queries; + self.media_query_sources = old_media_query_sources; + result + } + + fn with_environment( + &mut self, + env: Environment, + callback: impl FnOnce(&mut Self) -> T, + ) -> T { + let mut old_env = env; + mem::swap(&mut self.env, &mut old_env); + let val = callback(self); + mem::swap(&mut self.env, &mut old_env); + val + } + + fn add_child( + &mut self, + node: CssStmt, + through: Option bool>, + ) -> CssTreeIdx { + if self.parent.is_none() || self.parent == Some(CssTree::ROOT) { + return self.css_tree.add_stmt(node, self.parent); + } + + let mut parent = self.parent.unwrap(); + + if let Some(through) = through { + while parent != CssTree::ROOT && through(self.css_tree.get(parent).as_ref().unwrap()) { + let grandparent = self.css_tree.child_to_parent.get(&parent).copied(); + debug_assert!( + grandparent.is_some(), + "through() must return false for at least one parent of $node." + ); + parent = grandparent.unwrap(); + } + + // If the parent has a (visible) following sibling, we shouldn't add to + // the parent. Instead, we should create a copy and add it after the + // interstitial sibling. + if self.css_tree.has_following_sibling(parent) { + let grandparent = self.css_tree.child_to_parent.get(&parent).copied().unwrap(); + let parent_node = self + .css_tree + .get(parent) + .as_ref() + .map(CssStmt::copy_without_children) + .unwrap(); + parent = self.css_tree.add_child(parent_node, grandparent); + } + } + + self.css_tree.add_child(node, parent) + } + + fn with_parent( + &mut self, + parent: CssStmt, + // default=true + scope_when: bool, + callback: impl FnOnce(&mut Self) -> T, + // todo: optional + through: impl Fn(&CssStmt) -> bool, + ) -> T { + let parent_idx = self.add_child(parent, Some(through)); + let old_parent = self.parent; + self.parent = Some(parent_idx); + let result = self.with_scope(false, scope_when, callback); + self.parent = old_parent; + result + } + + fn with_scope( + &mut self, + // default=false + semi_global: bool, + // default=true + when: bool, + callback: impl FnOnce(&mut Self) -> T, + ) -> T { + let semi_global = semi_global && self.flags.in_semi_global_scope(); + let was_in_semi_global_scope = self.flags.in_semi_global_scope(); + self.flags + .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, semi_global); + + if !when { + let v = callback(self); + self.flags + .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, was_in_semi_global_scope); + + return v; + } + + self.env.scopes_mut().enter_new_scope(); + + let v = callback(self); + + self.flags + .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, was_in_semi_global_scope); + self.env.scopes_mut().exit_scope(); + + v + } + + fn with_content( + &mut self, + content: Option>, + callback: impl FnOnce(&mut Self) -> T, + ) -> T { + let old_content = self.env.content.take(); + self.env.content = content; + let v = callback(self); + self.env.content = old_content; + v + } + + fn visit_include_stmt(&mut self, include_stmt: AstInclude) -> SassResult> { + let mixin = self + .env + .get_mixin(include_stmt.name, include_stmt.namespace)?; + + match mixin { + Mixin::Builtin(mixin) => { + if include_stmt.content.is_some() { + return Err(("Mixin doesn't accept a content block.", include_stmt.span).into()); + } + + let args = self.eval_args(include_stmt.args, include_stmt.name.span)?; + mixin(args, self)?; + + Ok(None) + } + Mixin::UserDefined(mixin, env) => { + if include_stmt.content.is_some() && !mixin.has_content { + return Err(("Mixin doesn't accept a content block.", include_stmt.span).into()); + } + + let AstInclude { args, content, .. } = include_stmt; + + let old_in_mixin = self.flags.in_mixin(); + self.flags.set(ContextFlags::IN_MIXIN, true); + + let callable_content = content.map(|c| { + Arc::new(CallableContentBlock { + content: c, + env: self.env.new_closure(), + }) + }); + + self.run_user_defined_callable::<_, ()>( + MaybeEvaledArguments::Invocation(args), + mixin, + &env, + include_stmt.name.span, + |mixin, visitor| { + visitor.with_content(callable_content, |visitor| { + for stmt in mixin.body { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + Ok(()) + }) + }, + )?; + + self.flags.set(ContextFlags::IN_MIXIN, old_in_mixin); + + Ok(None) + } + } + } + + fn visit_mixin_decl(&mut self, mixin: AstMixin) { + self.env.insert_mixin( + mixin.name, + Mixin::UserDefined(mixin, self.env.new_closure()), + ); + } + + fn visit_each_stmt(&mut self, each_stmt: AstEach) -> SassResult> { + let list = self.visit_expr(each_stmt.list)?.as_list(); + + // todo: not setting semi_global: true maybe means we can't assign to global scope when declared as global + self.env.scopes_mut().enter_new_scope(); + + let mut result = None; + + 'outer: for val in list { + if each_stmt.variables.len() == 1 { + let val = self.without_slash(val); + self.env + .scopes_mut() + .insert_var_last(each_stmt.variables[0], val); + } else { + for (&var, val) in each_stmt.variables.iter().zip( + val.as_list() + .into_iter() + .chain(std::iter::once(Value::Null).cycle()), + ) { + let val = self.without_slash(val); + self.env.scopes_mut().insert_var_last(var, val); + } + } + + for stmt in each_stmt.body.clone() { + let val = self.visit_stmt(stmt)?; + if val.is_some() { + result = val; + break 'outer; + } + } + } + + self.env.scopes_mut().exit_scope(); + + Ok(result) + } + + fn visit_for_stmt(&mut self, for_stmt: AstFor) -> SassResult> { + let from_span = for_stmt.from.span; + let to_span = for_stmt.to.span; + let from_number = self + .visit_expr(for_stmt.from.node)? + .assert_number(from_span)?; + let to_number = self.visit_expr(for_stmt.to.node)?.assert_number(to_span)?; + + if !to_number.unit().comparable(from_number.unit()) { + // todo: better error message here + return Err(( + "to and from values have incompatible units", + from_span.merge(to_span), + ) + .into()); + } + + let from = from_number.num().assert_int(from_span)?; + let mut to = to_number + .num() + .convert(to_number.unit(), from_number.unit()) + .assert_int(to_span)?; + + let direction = if from > to { -1 } else { 1 }; + + if !for_stmt.is_exclusive { + to += direction; + } + + if from == to { + return Ok(None); + } + + // todo: self.with_scopes + self.env.scopes_mut().enter_new_scope(); + + let mut result = None; + + let mut i = from; + 'outer: while i != to { + self.env.scopes_mut().insert_var_last( + for_stmt.variable.node, + Value::Dimension(SassNumber { + num: Number::from(i), + unit: from_number.unit().clone(), + as_slash: None, + }), + ); + + for stmt in for_stmt.body.clone() { + let val = self.visit_stmt(stmt)?; + if val.is_some() { + result = val; + break 'outer; + } + } + + i += direction; + } + + self.env.scopes_mut().exit_scope(); + + Ok(result) + } + + fn visit_while_stmt(&mut self, while_stmt: &AstWhile) -> SassResult> { + self.with_scope::>>(true, true, |visitor| { + let mut result = None; + + 'outer: while visitor.visit_expr(while_stmt.condition.clone())?.is_true() { + for stmt in while_stmt.body.clone() { + let val = visitor.visit_stmt(stmt)?; + if val.is_some() { + result = val; + break 'outer; + } + } + } + + Ok(result) + }) + } + + fn visit_if_stmt(&mut self, if_stmt: AstIf) -> SassResult> { + let mut clause: Option> = if_stmt.else_clause; + for clause_to_check in if_stmt.if_clauses { + if self.visit_expr(clause_to_check.condition)?.is_true() { + clause = Some(clause_to_check.body); + break; + } + } + + // todo: self.with_scope + self.env.scopes_mut().enter_new_scope(); + + let mut result = None; + + if let Some(stmts) = clause { + for stmt in stmts { + let val = self.visit_stmt(stmt)?; + if val.is_some() { + result = val; + break; + } + } + } + + self.env.scopes_mut().exit_scope(); + + Ok(result) + } + + fn visit_loud_comment(&mut self, comment: AstLoudComment) -> SassResult> { + if self.flags.in_function() { + return Ok(None); + } + + // todo: Comments are allowed to appear between CSS imports + // if (_parent == _root && _endOfImports == _root.children.length) { + // _endOfImports++; + // } + + let comment = CssStmt::Comment( + self.perform_interpolation(comment.text, false)?, + comment.span, + ); + self.css_tree.add_stmt(comment, self.parent); + + Ok(None) + } + + fn visit_variable_decl(&mut self, decl: AstVariableDecl) -> SassResult> { + let name = Spanned { + node: decl.name, + span: decl.span, + }; + + if decl.is_guarded { + if decl.namespace.is_none() && self.env.at_root() { + let var_override = (*self.configuration).borrow_mut().remove(decl.name); + if !matches!( + var_override, + Some(ConfiguredValue { + value: Value::Null, + .. + }) | None + ) { + self.env.insert_var( + name, + None, + var_override.unwrap().value, + true, + self.flags.in_semi_global_scope(), + )?; + return Ok(None); + } + } + + if self.env.var_exists(decl.name, decl.namespace)? { + let value = self.env.get_var(name, decl.namespace).unwrap(); + + if value != Value::Null { + return Ok(None); + } + } + } + + let value = self.visit_expr(decl.value)?; + let value = self.without_slash(value); + + self.env.insert_var( + name, + decl.namespace, + value, + decl.is_global, + self.flags.in_semi_global_scope(), + )?; + + Ok(None) + } + + fn interpolation_to_value( + &mut self, + interpolation: Interpolation, + // default=false + trim: bool, + // default=false + warn_for_color: bool, + ) -> SassResult { + let result = self.perform_interpolation(interpolation, warn_for_color)?; + + Ok(if trim { + trim_ascii(&result, true).to_owned() + } else { + result + }) + } + + fn perform_interpolation( + &mut self, + interpolation: Interpolation, + // todo check to emit warning if this is true + _warn_for_color: bool, + ) -> SassResult { + // todo: potential optimization for contents len == 1 and no exprs + + let result = interpolation.contents.into_iter().map(|part| match part { + InterpolationPart::String(s) => Ok(s), + InterpolationPart::Expr(e) => { + let span = e.span; + let result = self.visit_expr(e.node)?; + // todo: span for specific expr + self.serialize(result, QuoteKind::None, span) + } + }); + + result.collect() + } + + fn evaluate_to_css( + &mut self, + expr: AstExpr, + quote: QuoteKind, + span: Span, + ) -> SassResult { + let result = self.visit_expr(expr)?; + self.serialize(result, quote, span) + } + + #[allow(clippy::unused_self)] + fn without_slash(&mut self, v: Value) -> Value { + match v { + Value::Dimension(SassNumber { .. }) if v.as_slash().is_some() => { + // todo: emit warning. we don't currently because it can be quite loud + // self.emit_warning( + // Cow::Borrowed("Using / for division is deprecated and will be removed at some point in the future"), + // self.span_before, + // ); + } + _ => {} + } + + v.without_slash() + } + + fn eval_maybe_args( + &mut self, + args: MaybeEvaledArguments, + span: Span, + ) -> SassResult { + match args { + MaybeEvaledArguments::Invocation(args) => self.eval_args(args, span), + MaybeEvaledArguments::Evaled(args) => Ok(args), + } + } + + fn eval_args( + &mut self, + arguments: ArgumentInvocation, + span: Span, + ) -> SassResult { + let mut positional = Vec::new(); + + for expr in arguments.positional { + let val = self.visit_expr(expr)?; + positional.push(self.without_slash(val)); + } + + let mut named = BTreeMap::new(); + + for (key, expr) in arguments.named { + let val = self.visit_expr(expr)?; + named.insert(key, self.without_slash(val)); + } + + if arguments.rest.is_none() { + return Ok(ArgumentResult { + positional, + named, + separator: ListSeparator::Undecided, + span, + touched: BTreeSet::new(), + }); + } + + let rest = self.visit_expr(arguments.rest.unwrap())?; + + let mut separator = ListSeparator::Undecided; + + match rest { + Value::Map(rest) => self.add_rest_map(&mut named, rest)?, + Value::List(elems, list_separator, _) => { + let mut list = elems + .into_iter() + .map(|e| self.without_slash(e)) + .collect::>(); + positional.append(&mut list); + separator = list_separator; + } + Value::ArgList(arglist) => { + // todo: superfluous clone + for (&key, value) in arglist.keywords() { + named.insert(key, self.without_slash(value.clone())); + } + + let mut list = arglist + .elems + .into_iter() + .map(|e| self.without_slash(e)) + .collect::>(); + positional.append(&mut list); + separator = arglist.separator; + } + _ => { + positional.push(self.without_slash(rest)); + } + } + + if arguments.keyword_rest.is_none() { + return Ok(ArgumentResult { + positional, + named, + separator: ListSeparator::Undecided, + span: arguments.span, + touched: BTreeSet::new(), + }); + } + + match self.visit_expr(arguments.keyword_rest.unwrap())? { + Value::Map(keyword_rest) => { + self.add_rest_map(&mut named, keyword_rest)?; + + Ok(ArgumentResult { + positional, + named, + separator, + span: arguments.span, + touched: BTreeSet::new(), + }) + } + v => { + return Err(( + format!( + "Variable keyword arguments must be a map (was {}).", + v.inspect(arguments.span)? + ), + arguments.span, + ) + .into()); + } + } + } + + fn add_rest_map( + &mut self, + named: &mut BTreeMap, + rest: SassMap, + ) -> SassResult<()> { + for (key, val) in rest { + match key.node { + Value::String(text, ..) => { + let val = self.without_slash(val); + named.insert(Identifier::from(text), val); + } + _ => { + return Err(( + // todo: we have to render the map for this error message + "Variable keyword argument map must have string keys.", + key.span, + ) + .into()); + } + } + } + + Ok(()) + } + + fn run_user_defined_callable( + &mut self, + arguments: MaybeEvaledArguments, + func: F, + env: &Environment, + span: Span, + run: impl FnOnce(F, &mut Self) -> SassResult, + ) -> SassResult { + let mut evaluated = self.eval_maybe_args(arguments, span)?; + + let mut name = func.name().to_string(); + + if name != "@content" { + name.push_str("()"); + } + + self.with_environment::>(env.new_closure(), |visitor| { + visitor.with_scope(false, true, move |visitor| { + func.arguments().verify( + evaluated.positional.len(), + &evaluated.named, + evaluated.span, + )?; + + // todo: superfluous clone + let declared_arguments = func.arguments().args.clone(); + let min_len = evaluated.positional.len().min(declared_arguments.len()); + + #[allow(clippy::needless_range_loop)] + for i in 0..min_len { + // todo: superfluous clone + visitor.env.scopes_mut().insert_var_last( + declared_arguments[i].name, + evaluated.positional[i].clone(), + ); + } + + // todo: better name for var + let additional_declared_args = + if declared_arguments.len() > evaluated.positional.len() { + &declared_arguments[evaluated.positional.len()..declared_arguments.len()] + } else { + &[] + }; + + for argument in additional_declared_args { + let name = argument.name; + let value = evaluated.named.remove(&argument.name).map_or_else( + || { + // todo: superfluous clone + let v = visitor.visit_expr(argument.default.clone().unwrap())?; + Ok(visitor.without_slash(v)) + }, + SassResult::Ok, + )?; + visitor.env.scopes_mut().insert_var_last(name, value); + } + + let were_keywords_accessed = Arc::new(Cell::new(false)); + + let argument_list = if let Some(rest_arg) = func.arguments().rest { + let rest = if evaluated.positional.len() > declared_arguments.len() { + &evaluated.positional[declared_arguments.len()..] + } else { + &[] + }; + + let arg_list = Value::ArgList(ArgList::new( + rest.to_vec(), + // todo: superfluous clone + Arc::clone(&were_keywords_accessed), + evaluated.named.clone(), + if evaluated.separator == ListSeparator::Undecided { + ListSeparator::Comma + } else { + ListSeparator::Space + }, + )); + + visitor + .env + .scopes_mut() + // todo: superfluous clone + .insert_var_last(rest_arg, arg_list.clone()); + + Some(arg_list) + } else { + None + }; + + let val = run(func, visitor)?; + + if argument_list.is_none() || evaluated.named.is_empty() { + return Ok(val); + } + + if (*were_keywords_accessed).get() { + return Ok(val); + } + + let argument_word = if evaluated.named.len() == 1 { + "argument" + } else { + "arguments" + }; + + let argument_names = to_sentence( + evaluated + .named + .keys() + .map(|key| format!("${key}")) + .collect(), + "or", + ); + + Err((format!("No {argument_word} named {argument_names}."), span).into()) + }) + }) + } + + pub(crate) fn run_function_callable( + &mut self, + func: SassFunction, + arguments: ArgumentInvocation, + span: Span, + ) -> SassResult { + self.run_function_callable_with_maybe_evaled( + func, + MaybeEvaledArguments::Invocation(arguments), + span, + ) + } + + pub(crate) fn run_function_callable_with_maybe_evaled( + &mut self, + func: SassFunction, + arguments: MaybeEvaledArguments, + span: Span, + ) -> SassResult { + match func { + SassFunction::Builtin(func, _name) => { + let evaluated = self.eval_maybe_args(arguments, span)?; + let val = func.0(evaluated, self)?; + Ok(self.without_slash(val)) + } + SassFunction::UserDefined(UserDefinedFunction { function, env, .. }) => self + .run_user_defined_callable( + arguments, + *function, + &env, + span, + |function, visitor| { + for stmt in function.children { + let result = visitor.visit_stmt(stmt)?; + + if let Some(val) = result { + return Ok(val); + } + } + + Err(("Function finished without @return.", span).into()) + }, + ), + SassFunction::Plain { name } => { + let arguments = match arguments { + MaybeEvaledArguments::Invocation(args) => args, + MaybeEvaledArguments::Evaled(..) => unreachable!(), + }; + + if !arguments.named.is_empty() || arguments.keyword_rest.is_some() { + return Err( + ("Plain CSS functions don't support keyword arguments.", span).into(), + ); + } + + let mut buffer = format!("{}(", name.as_str()); + let mut first = true; + + for argument in arguments.positional { + if first { + first = false; + } else { + buffer.push_str(", "); + } + + buffer.push_str(&self.evaluate_to_css(argument, QuoteKind::Quoted, span)?); + } + + if let Some(rest_arg) = arguments.rest { + let rest = self.visit_expr(rest_arg)?; + if !first { + buffer.push_str(", "); + } + buffer.push_str(&self.serialize(rest, QuoteKind::Quoted, span)?); + } + buffer.push(')'); + + Ok(Value::String(buffer, QuoteKind::None)) + } + } + } + + fn visit_list_expr(&mut self, list: ListExpr) -> SassResult { + let elems = list + .elems + .into_iter() + .map(|e| { + let value = self.visit_expr(e.node)?; + Ok(value) + }) + .collect::>>()?; + + Ok(Value::List(elems, list.separator, list.brackets)) + } + + fn visit_function_call_expr(&mut self, func_call: FunctionCallExpr) -> SassResult { + let name = func_call.name; + + let func = match self.env.get_fn(name, func_call.namespace)? { + Some(func) => func, + None => { + if let Some(f) = GLOBAL_FUNCTIONS.get(name.as_str()) { + SassFunction::Builtin(f.clone(), name) + } else { + if func_call.namespace.is_some() { + return Err(("Undefined function.", func_call.span).into()); + } + + SassFunction::Plain { name } + } + } + }; + + let old_in_function = self.flags.in_function(); + self.flags.set(ContextFlags::IN_FUNCTION, true); + let value = self.run_function_callable(func, *func_call.arguments, func_call.span)?; + self.flags.set(ContextFlags::IN_FUNCTION, old_in_function); + + Ok(value) + } + + fn visit_interpolated_func_expr(&mut self, func: InterpolatedFunction) -> SassResult { + let InterpolatedFunction { + name, + arguments: args, + span, + } = func; + let fn_name = self.perform_interpolation(name, false)?; + + if !args.named.is_empty() || args.keyword_rest.is_some() { + return Err(("Plain CSS functions don't support keyword arguments.", span).into()); + } + + let mut buffer = format!("{}(", fn_name); + + let mut first = true; + for arg in args.positional { + if first { + first = false; + } else { + buffer.push_str(", "); + } + let evaluated = self.evaluate_to_css(arg, QuoteKind::Quoted, span)?; + buffer.push_str(&evaluated); + } + + if let Some(rest_arg) = args.rest { + let rest = self.visit_expr(rest_arg)?; + if !first { + buffer.push_str(", "); + } + buffer.push_str(&self.serialize(rest, QuoteKind::None, span)?); + } + + buffer.push(')'); + + Ok(Value::String(buffer, QuoteKind::None)) + } + + fn visit_parent_selector(&self) -> Value { + match &self.style_rule_ignoring_at_root { + Some(selector) => selector.as_selector_list().clone().to_sass_list(), + None => Value::Null, + } + } + + fn visit_expr(&mut self, expr: AstExpr) -> SassResult { + Ok(match expr { + AstExpr::Color(color) => Value::Color(color), + AstExpr::Number { n, unit } => Value::Dimension(SassNumber { + num: n, + unit, + as_slash: None, + }), + AstExpr::List(list) => self.visit_list_expr(list)?, + AstExpr::String(StringExpr(text, quote), ..) => self.visit_string(text, quote)?, + AstExpr::BinaryOp { + lhs, + op, + rhs, + allows_slash, + span, + } => self.visit_bin_op(*lhs, op, *rhs, allows_slash, span)?, + AstExpr::True => Value::True, + AstExpr::False => Value::False, + AstExpr::Calculation { name, args } => { + self.visit_calculation_expr(name, args, self.span_before)? + } + AstExpr::FunctionCall(func_call) => self.visit_function_call_expr(func_call)?, + AstExpr::If(if_expr) => self.visit_ternary(*if_expr)?, + AstExpr::InterpolatedFunction(func) => self.visit_interpolated_func_expr(func)?, + AstExpr::Map(map) => self.visit_map(map)?, + AstExpr::Null => Value::Null, + AstExpr::Paren(expr) => self.visit_expr(*expr)?, + AstExpr::ParentSelector => self.visit_parent_selector(), + AstExpr::UnaryOp(op, expr, span) => self.visit_unary_op(op, *expr, span)?, + AstExpr::Variable { name, namespace } => self.env.get_var(name, namespace)?, + AstExpr::Supports(condition) => { + Value::String(self.visit_supports_condition(*condition)?, QuoteKind::None) + } + }) + } + + fn visit_calculation_value( + &mut self, + expr: AstExpr, + in_min_or_max: bool, + span: Span, + ) -> SassResult { + Ok(match expr { + AstExpr::Paren(inner) => match &*inner { + AstExpr::FunctionCall(FunctionCallExpr { ref name, .. }) + if name.as_str().to_ascii_lowercase() == "var" => + { + let result = self.visit_calculation_value(*inner, in_min_or_max, span)?; + + if let CalculationArg::String(text) = result { + CalculationArg::String(format!("({})", text)) + } else { + result + } + } + _ => self.visit_calculation_value(*inner, in_min_or_max, span)?, + }, + AstExpr::String(string_expr, _span) => { + debug_assert!(string_expr.1 == QuoteKind::None); + CalculationArg::Interpolation(self.perform_interpolation(string_expr.0, false)?) + } + AstExpr::BinaryOp { lhs, op, rhs, .. } => SassCalculation::operate_internal( + op, + self.visit_calculation_value(*lhs, in_min_or_max, span)?, + self.visit_calculation_value(*rhs, in_min_or_max, span)?, + in_min_or_max, + !self.flags.in_supports_declaration(), + self.options, + span, + )?, + AstExpr::Number { .. } + | AstExpr::Calculation { .. } + | AstExpr::Variable { .. } + | AstExpr::FunctionCall { .. } + | AstExpr::If(..) => { + let result = self.visit_expr(expr)?; + match result { + Value::Dimension(SassNumber { + num, + unit, + as_slash, + }) => CalculationArg::Number(SassNumber { + num, + unit, + as_slash, + }), + Value::Calculation(calc) => CalculationArg::Calculation(calc), + Value::String(s, quotes) if quotes == QuoteKind::None => { + CalculationArg::String(s) + } + value => { + return Err(( + format!( + "Value {} can't be used in a calculation.", + value.inspect(span)? + ), + span, + ) + .into()) + } + } + } + v => unreachable!("{:?}", v), + }) + } + + fn visit_calculation_expr( + &mut self, + name: CalculationName, + args: Vec, + span: Span, + ) -> SassResult { + let mut args = args + .into_iter() + .map(|arg| self.visit_calculation_value(arg, name.in_min_or_max(), span)) + .collect::>>()?; + + if self.flags.in_supports_declaration() { + return Ok(Value::Calculation(SassCalculation::unsimplified( + name, args, + ))); + } + + match name { + CalculationName::Calc => { + debug_assert_eq!(args.len(), 1); + Ok(SassCalculation::calc(args.remove(0))) + } + CalculationName::Min => SassCalculation::min(args, self.options, span), + CalculationName::Max => SassCalculation::max(args, self.options, span), + CalculationName::Clamp => { + let min = args.remove(0); + let value = if args.is_empty() { + None + } else { + Some(args.remove(0)) + }; + let max = if args.is_empty() { + None + } else { + Some(args.remove(0)) + }; + SassCalculation::clamp(min, value, max, self.options, span) + } + } + } + + fn visit_unary_op(&mut self, op: UnaryOp, expr: AstExpr, span: Span) -> SassResult { + let operand = self.visit_expr(expr)?; + + match op { + UnaryOp::Plus => operand.unary_plus(self, span), + UnaryOp::Neg => operand.unary_neg(self, span), + UnaryOp::Div => operand.unary_div(self, span), + UnaryOp::Not => Ok(operand.unary_not()), + } + } + + fn visit_ternary(&mut self, if_expr: Ternary) -> SassResult { + if_arguments().verify(if_expr.0.positional.len(), &if_expr.0.named, if_expr.0.span)?; + + let mut positional = if_expr.0.positional; + let mut named = if_expr.0.named; + + let condition = if positional.is_empty() { + named.remove(&Identifier::from("condition")).unwrap() + } else { + positional.remove(0) + }; + + let if_true = if positional.is_empty() { + named.remove(&Identifier::from("if_true")).unwrap() + } else { + positional.remove(0) + }; + + let if_false = if positional.is_empty() { + named.remove(&Identifier::from("if_false")).unwrap() + } else { + positional.remove(0) + }; + + let value = if self.visit_expr(condition)?.is_true() { + self.visit_expr(if_true)? + } else { + self.visit_expr(if_false)? + }; + + Ok(self.without_slash(value)) + } + + fn visit_string(&mut self, text: Interpolation, quote: QuoteKind) -> SassResult { + // Don't use [performInterpolation] here because we need to get the raw text + // from strings, rather than the semantic value. + let old_in_supports_declaration = self.flags.in_supports_declaration(); + self.flags.set(ContextFlags::IN_SUPPORTS_DECLARATION, false); + + let result = text + .contents + .into_iter() + .map(|part| match part { + InterpolationPart::String(s) => Ok(s), + InterpolationPart::Expr(Spanned { node, span }) => match self.visit_expr(node)? { + Value::String(s, ..) => Ok(s), + e => self.serialize(e, QuoteKind::None, span), + }, + }) + .collect::>()?; + + self.flags.set( + ContextFlags::IN_SUPPORTS_DECLARATION, + old_in_supports_declaration, + ); + + Ok(Value::String(result, quote)) + } + + fn visit_map(&mut self, map: AstSassMap) -> SassResult { + let mut sass_map = SassMap::new(); + + for pair in map.0 { + let key_span = pair.0.span; + let key = self.visit_expr(pair.0.node)?; + let value = self.visit_expr(pair.1)?; + + if sass_map.get_ref(&key).is_some() { + return Err(("Duplicate key.", key_span).into()); + } + + sass_map.insert( + Spanned { + node: key, + span: key_span, + }, + value, + ); + } + + Ok(Value::Map(sass_map)) + } + + fn visit_bin_op( + &mut self, + lhs: AstExpr, + op: BinaryOp, + rhs: AstExpr, + allows_slash: bool, + span: Span, + ) -> SassResult { + let left = self.visit_expr(lhs)?; + + Ok(match op { + BinaryOp::SingleEq => { + let right = self.visit_expr(rhs)?; + single_eq(&left, &right, self.options, span)? + } + BinaryOp::Or => { + if left.is_true() { + left + } else { + self.visit_expr(rhs)? + } + } + BinaryOp::And => { + if left.is_true() { + self.visit_expr(rhs)? + } else { + left + } + } + BinaryOp::Equal => { + let right = self.visit_expr(rhs)?; + Value::bool(left == right) + } + BinaryOp::NotEqual => { + let right = self.visit_expr(rhs)?; + Value::bool(left != right) + } + BinaryOp::GreaterThan + | BinaryOp::GreaterThanEqual + | BinaryOp::LessThan + | BinaryOp::LessThanEqual => { + let right = self.visit_expr(rhs)?; + cmp(&left, &right, self.options, span, op)? + } + BinaryOp::Plus => { + let right = self.visit_expr(rhs)?; + add(left, right, self.options, span)? + } + BinaryOp::Minus => { + let right = self.visit_expr(rhs)?; + sub(left, right, self.options, span)? + } + BinaryOp::Mul => { + let right = self.visit_expr(rhs)?; + mul(left, right, self.options, span)? + } + BinaryOp::Div => { + let right = self.visit_expr(rhs)?; + + let left_is_number = matches!(left, Value::Dimension { .. }); + let right_is_number = matches!(right, Value::Dimension { .. }); + + let result = div(left.clone(), right.clone(), self.options, span)?; + + if left_is_number && right_is_number && allows_slash { + return result.with_slash( + left.assert_number(span)?, + right.assert_number(span)?, + span, + ); + } else if left_is_number && right_is_number { + // todo: emit warning here. it prints too frequently, so we do not currently + // self.emit_warning( + // Cow::Borrowed(format!( + // "Using / for division outside of calc() is deprecated" + // )), + // span, + // ); + } + + result + } + BinaryOp::Rem => { + let right = self.visit_expr(rhs)?; + rem(left, right, self.options, span)? + } + }) + } + + // todo: superfluous taking `expr` by value + fn serialize(&mut self, mut expr: Value, quote: QuoteKind, span: Span) -> SassResult { + if quote == QuoteKind::None { + expr = expr.unquote(); + } + + Ok(expr + .to_css_string(span, self.options.is_compressed())? + .into_owned()) + } + + pub fn visit_ruleset(&mut self, ruleset: AstRuleSet) -> SassResult> { + if self.declaration_name.is_some() { + return Err(( + "Style rules may not be used within nested declarations.", + ruleset.span, + ) + .into()); + } + + let AstRuleSet { + selector: ruleset_selector, + body: ruleset_body, + .. + } = ruleset; + + let selector_text = self.interpolation_to_value(ruleset_selector, true, true)?; + + if self.flags.in_keyframes() { + let sel_toks = Lexer::new( + selector_text + .chars() + .map(|x| Token::new(self.span_before, x)) + .collect(), + ); + let parsed_selector = + KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?; + + let keyframes_ruleset = CssStmt::KeyframesRuleSet(KeyframesRuleSet { + selector: parsed_selector, + body: Vec::new(), + }); + + self.with_parent::>( + keyframes_ruleset, + true, + |visitor| { + for stmt in ruleset_body { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + }, + CssStmt::is_style_rule, + )?; + + return Ok(None); + } + + let mut parsed_selector = self.parse_selector_from_string( + &selector_text, + !self.is_plain_css, + !self.is_plain_css, + ruleset.selector_span, + )?; + + parsed_selector = parsed_selector.resolve_parent_selectors( + self.style_rule_ignoring_at_root + .as_ref() + // todo: this clone should be superfluous(?) + .map(|x| x.as_selector_list().clone()), + !self.flags.at_root_excluding_style_rule(), + )?; + + // todo: _mediaQueries + let selector = self + .extender + .add_selector(parsed_selector, &self.media_queries); + + let rule = CssStmt::RuleSet { + selector: selector.clone(), + body: Vec::new(), + is_group_end: false, + }; + + let old_at_root_excluding_style_rule = self.flags.at_root_excluding_style_rule(); + + self.flags + .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, false); + + let old_style_rule_ignoring_at_root = self.style_rule_ignoring_at_root.take(); + self.style_rule_ignoring_at_root = Some(selector); + + self.with_parent::>( + rule, + true, + |visitor| { + for stmt in ruleset_body { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + }, + CssStmt::is_style_rule, + )?; + + self.style_rule_ignoring_at_root = old_style_rule_ignoring_at_root; + self.flags.set( + ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, + old_at_root_excluding_style_rule, + ); + + self.set_group_end(); + + Ok(None) + } + + fn set_group_end(&mut self) -> Option<()> { + if !self.style_rule_exists() { + let children = self + .css_tree + .parent_to_child + .get(&self.parent.unwrap_or(CssTree::ROOT))?; + let child = *children.last()?; + self.css_tree + .get_mut(child) + .as_mut() + .map(CssStmt::set_group_end)?; + } + + Some(()) + } + + fn style_rule_exists(&self) -> bool { + !self.flags.at_root_excluding_style_rule() && self.style_rule_ignoring_at_root.is_some() + } + + pub fn visit_style(&mut self, style: AstStyle) -> SassResult> { + if !self.style_rule_exists() + && !self.flags.in_unknown_at_rule() + && !self.flags.in_keyframes() + { + return Err(( + "Declarations may only be used within style rules.", + style.span, + ) + .into()); + } + + let is_custom_property = style.is_custom_property(); + + let mut name = self.interpolation_to_value(style.name, false, true)?; + + if let Some(declaration_name) = &self.declaration_name { + name = format!("{}-{}", declaration_name, name); + } + + if let Some(value) = style + .value + .map(|s| { + SassResult::Ok(Spanned { + node: self.visit_expr(s.node)?, + span: s.span, + }) + }) + .transpose()? + { + // If the value is an empty list, preserve it, because converting it to CSS + // will throw an error that we want the user to see. + if !value.is_null() || value.is_empty_list() { + // todo: superfluous clones? + self.css_tree.add_stmt( + CssStmt::Style(Style { + property: InternedString::get_or_intern(&name), + value: Box::new(value), + declared_as_custom_property: is_custom_property, + }), + self.parent, + ); + } else if name.starts_with("--") { + return Err(("Custom property values may not be empty.", style.span).into()); + } + } + + let children = style.body; + + if !children.is_empty() { + let old_declaration_name = self.declaration_name.take(); + self.declaration_name = Some(name); + self.with_scope::>(false, true, |visitor| { + for stmt in children { + let result = visitor.visit_stmt(stmt)?; + debug_assert!(result.is_none()); + } + + Ok(()) + })?; + self.declaration_name = old_declaration_name; + } + + Ok(None) + } +} diff --git a/src/fs.rs b/src/fs.rs index 24d8ad5a..9dced3c6 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -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>; + fn read(&self, path: &Path) -> io::Result>; } /// Use [`std::fs`] to read any files from disk. @@ -36,7 +38,7 @@ impl Fs for StdFs { } #[inline] - fn read(&self, path: &Path) -> Result> { + fn read(&self, path: &Path) -> io::Result> { std::fs::read(path) } } @@ -61,7 +63,7 @@ impl Fs for NullFs { } #[inline] - fn read(&self, _path: &Path) -> Result> { + fn read(&self, _path: &Path) -> io::Result> { Err(Error::new( ErrorKind::NotFound, "NullFs, there is no file system", diff --git a/src/interner.rs b/src/interner.rs index c394b7f8..5a2c37fc 100644 --- a/src/interner.rs +++ b/src/interner.rs @@ -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)) } } diff --git a/src/lexer.rs b/src/lexer.rs index c088f739..a4cc3617 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -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 peek(&self) -> Option { - self.buf.get(self.peek_cursor()).copied() + pub fn next_char_is(&self, c: char) -> bool { + matches!(self.peek(), Some(Token { kind, .. }) if kind == c) } - pub fn reset_cursor(&mut self) { - self.amt_peeked = 0; - } + 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; - pub fn peek_next(&mut self) -> Option { - self.amt_peeked += 1; + start.merge(end) + } - self.peek() + 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 peek_previous(&mut self) -> Option { - self.buf.get(self.peek_cursor().checked_sub(1)?).copied() + pub fn current_span(&self) -> Span { + self.buf + .get(self.cursor) + .copied() + .unwrap_or_else(|| self.buf.last().copied().unwrap()) + .pos } - pub fn peek_forward(&mut self, n: usize) -> Option { - self.amt_peeked += n; + pub fn peek(&self) -> Option { + self.buf.get(self.cursor).copied() + } - self.peek() + /// Peeks the previous token without modifying the peek cursor + pub fn peek_previous(&mut self) -> Option { + 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 { - self.buf.get(self.peek_cursor() + n).copied() + self.buf.get(self.cursor + n).copied() } - pub fn peek_backward(&mut self, n: usize) -> Option { - 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 { + 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.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, } } } diff --git a/src/lib.rs b/src/lib.rs index a2a9146a..b082dea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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> { - 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,237 +54,116 @@ 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, - } - } +fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box { + let (message, span) = err.raw(); + Box::new(Error::from_loc(message, map.look_up_span(span), unicode)) } -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 - } +fn from_string_with_file_name(input: String, file_name: &str, options: &Options) -> Result { + 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); - /// `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 - } + let path = Path::new(file_name); - /// 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 - } + let input_syntax = options + .input_syntax + .unwrap_or_else(|| InputSyntax::for_path(path)); - /// 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 - } + 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() + } + }; - /// 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 - } + let stylesheet = match stylesheet { + Ok(v) => v, + Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)), + }; - /// 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 + let mut visitor = Visitor::new(path, options, &mut map, empty_span); + match visitor.visit_stylesheet(stylesheet) { + Ok(_) => {} + Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)), } + let stmts = visitor.finish(); - /// 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 - } + let mut serializer = Serializer::new(options, &map, false, empty_span); - pub(crate) fn is_compressed(&self) -> bool { - matches!(self.style, OutputStyle::Compressed) - } -} + let mut prev_was_group_end = false; + let mut prev_requires_semicolon = false; + for stmt in stmts { + if stmt.is_invisible() { + continue; + } -fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box { - let (message, span) = err.raw(); - Box::new(Error::from_loc(message, map.look_up_span(span), unicode)) -} + let is_group_end = stmt.is_group_end(); + let requires_semicolon = Serializer::requires_semicolon(&stmt); -fn from_string_with_file_name(input: String, file_name: &str, options: &Options) -> Result { - let mut map = CodeMap::new(); - let file = map.add_file(file_name.to_owned(), input); - let empty_span = file.span.subspan(0, 0); + serializer + .visit_group(stmt, prev_was_group_end, prev_requires_semicolon) + .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; - 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(), + prev_was_group_end = is_group_end; + prev_requires_semicolon = requires_semicolon; } - .parse() - .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; - 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)) + 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 { 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 { /// Ok(()) /// } /// ``` -#[cfg_attr(feature = "profiling", inline(never))] -#[cfg_attr(not(feature = "profiling"), inline)] + +#[inline] pub fn from_string(input: String, options: &Options) -> Result { from_string_with_file_name(input, "stdin", options) } diff --git a/src/main.rs b/src/main.rs index 9b5927ce..fcf8ebbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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") diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 00000000..d891acbb --- /dev/null +++ b/src/options.rs @@ -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, +} + +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, +} diff --git a/src/output.rs b/src/output.rs deleted file mode 100644 index 629a4f3f..00000000 --- a/src/output.rs +++ /dev/null @@ -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, - 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, - is_group_end: bool, - }, - MultilineComment(String), - UnknownAtRule(Box), - Keyframes(Box), - KeyframesRuleSet(Vec, Vec), - Media { - query: String, - body: Vec, - inside_rule: bool, - is_group_end: bool, - }, - Supports { - params: String, - body: Vec, - 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 { - 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) -> 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, - at_rule_context: AtRuleContext, - allows_charset: bool, - plain_imports: Vec, -} - -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, - at_rule_context: AtRuleContext, - allows_charset: bool, - ) -> SassResult { - Css::new(at_rule_context, allows_charset).parse_stylesheet(s) - } - - fn parse_stmt(&mut self, stmt: Stmt) -> SassResult> { - 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::>>>()? - .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) -> SassResult { - 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 { - 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, 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, 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, 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, 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, css: Css, map: &CodeMap) -> SassResult<()> { - let padding = " ".repeat(self.nesting); - self.nesting += 1; - - let mut prev: Option = 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::>() - .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(()) - } -} diff --git a/src/parse/args.rs b/src/parse/args.rs deleted file mode 100644 index 13dde73f..00000000 --- a/src/parse/args.rs +++ /dev/null @@ -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 { - let mut args: Vec = 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 = 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 { - 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 { - 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) - } -} diff --git a/src/parse/at_root_query.rs b/src/parse/at_root_query.rs new file mode 100644 index 00000000..190e80b2 --- /dev/null +++ b/src/parse/at_root_query.rs @@ -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 { + 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)) + } +} diff --git a/src/parse/base.rs b/src/parse/base.rs new file mode 100644 index 00000000..93833168 --- /dev/null +++ b/src/parse/base.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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> { + 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(&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( + &mut self, + func: impl Fn(&mut Self) -> SassResult, + ) -> SassResult { + 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 { + 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 { + 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 { + 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 { + 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(); + } + } +} diff --git a/src/parse/common.rs b/src/parse/common.rs deleted file mode 100644 index d1238b0d..00000000 --- a/src/parse/common.rs +++ /dev/null @@ -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 { - first: T, - rest: Vec, -} - -impl NeverEmptyVec { - 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 { - 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>>), - 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 for u8 { - type Output = Self; - #[inline] - fn bitand(self, rhs: ContextFlag) -> Self::Output { - self & rhs.0 - } -} - -impl BitOr for ContextFlags { - type Output = Self; - fn bitor(self, rhs: ContextFlag) -> Self::Output { - Self(self.0 | rhs.0) - } -} - -pub(crate) enum Comment { - Silent, - Loud(String), -} diff --git a/src/parse/control_flow.rs b/src/parse/control_flow.rs deleted file mode 100644 index 31bd62a7..00000000 --- a/src/parse/control_flow.rs +++ /dev/null @@ -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> { - 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> { - 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 = 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> { - // 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> { - let mut vars: Vec> = 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) - } -} diff --git a/src/parse/css.rs b/src/parse/css.rs new file mode 100644 index 00000000..0fa68e11 --- /dev/null +++ b/src/parse/css.rs @@ -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 SassResult>> = + Some(Self::parse_identifier_like); + + fn parse_at_rule( + &mut self, + _child: fn(&mut Self) -> SassResult, + ) -> SassResult { + 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 { + 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> { + 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)) + } +} diff --git a/src/parse/function.rs b/src/parse/function.rs deleted file mode 100644 index edf0ee63..00000000 --- a/src/parse/function.rs +++ /dev/null @@ -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> { - 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>, - ) -> SassResult { - 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"), - } - } -} diff --git a/src/parse/ident.rs b/src/parse/ident.rs deleted file mode 100644 index 17f5e422..00000000 --- a/src/parse/ident.rs +++ /dev/null @@ -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> { - 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 { - 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> { - 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> { - 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> { - 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 - } - } - } -} diff --git a/src/parse/import.rs b/src/parse/import.rs deleted file mode 100644 index 79e18c45..00000000 --- a/src/parse/import.rs +++ /dev/null @@ -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. - /// - /// - /// - pub(super) fn find_import(&self, path: &Path) -> Option { - 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> { - 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> { - 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 = 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()), - } - } -} diff --git a/src/parse/keyframes.rs b/src/parse/keyframes.rs index d4000d3b..0a638c53 100644 --- a/src/parse/keyframes.rs +++ b/src/parse/keyframes.rs @@ -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> { - 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()); - } - } - '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()); - } + fn toks_mut(&mut self) -> &mut Lexer<'a> { + &mut self.toks + } +} - self.parser.expect_char('%')?; +impl<'a> KeyframesSelectorParser<'a> { + pub fn new(toks: Lexer<'a>) -> KeyframesSelectorParser<'a> { + KeyframesSelectorParser { toks } + } - selectors.push(KeyframesSelector::Percent(num.into_boxed_str())); + pub fn parse_keyframes_selector(&mut self) -> SassResult> { + let mut selectors = Vec::new(); + 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()); } - '{' => 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 { - 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 { + 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> { - let mut span = if let Some(tok) = self.toks.peek() { - tok.pos() - } else { - return Err(("expected \"{\".", self.span_before).into()); - }; - - self.span_before = span; - - while let Some(tok) = self.toks.next() { - span = span.merge(tok.pos()); - match tok.kind { - '#' => { - if self.consume_char_if_exists('{') { - string.push_str( - &self - .parse_interpolation()? - .to_css_string(span, self.options.is_compressed())?, - ); - } else { - string.push('#'); - } - } - ',' => { - while let Some(c) = string.pop() { - if c == ' ' || c == ',' { - continue; - } - string.push(c); - string.push(','); - break; - } - } - '/' => { - if self.toks.peek().is_none() { - return Err(("Expected selector.", tok.pos()).into()); - } - self.parse_comment()?; - self.whitespace(); - string.push(' '); - } - '{' => { - let sel_toks: Vec = - string.chars().map(|x| Token::new(span, x)).collect(); - - let selector = KeyframesSelectorParser::new(&mut Parser { - toks: &mut Lexer::new(sel_toks), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - }) - .parse_keyframes_selector()?; - - return Ok(selector); - } - c => string.push(c), - } + if !matches!( + self.toks.peek(), + Some(Token { + kind: '0'..='9' | '.', + .. + }) + ) { + return Err(("Expected number.", self.toks.current_span()).into()); } - Err(("expected \"{\".", span).into()) - } + while matches!( + self.toks.peek(), + Some(Token { + kind: '0'..='9', + .. + }) + ) { + buffer.push(self.toks.next().unwrap().kind); + } - pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult { - if self.flags.in_function() { - return Err(("This at-rule is not allowed here.", self.span_before).into()); + if self.scan_char('.') { + buffer.push('.'); + + while matches!( + self.toks.peek(), + Some(Token { + kind: '0'..='9', + .. + }) + ) { + buffer.push(self.toks.next().unwrap().kind); + } } - let name = self.parse_keyframes_name()?; - - 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, + if self.scan_ident_char('e', false)? { + buffer.push('e'); + + if matches!( + self.toks.peek(), + Some(Token { + kind: '+' | '-', + .. + }) + ) { + buffer.push(self.toks.next().unwrap().kind); + } + + 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); + } } - .parse_stmt()?; - Ok(Stmt::Keyframes(Box::new(Keyframes { rule, name, body }))) + self.expect_char('%')?; + + Ok(KeyframesSelector::Percent(buffer.into_boxed_str())) } } diff --git a/src/parse/media.rs b/src/parse/media.rs deleted file mode 100644 index 6101cc8e..00000000 --- a/src/parse/media.rs +++ /dev/null @@ -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> { - 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 { - 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 { - 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 { - 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) - } -} diff --git a/src/parse/media_query.rs b/src/parse/media_query.rs new file mode 100644 index 00000000..53d3c177 --- /dev/null +++ b/src/parse/media_query.rs @@ -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> { + 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 { + 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 = None; + let media_type: Option; + 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 { + 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> { + 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()?; + } + } +} diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs deleted file mode 100644 index 79dccae6..00000000 --- a/src/parse/mixin.rs +++ /dev/null @@ -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> { - 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> { - 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() - }) - } -} diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 9a0e7029..90a0a64b 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,983 +1,44 @@ -use std::{convert::TryFrom, path::Path}; - -use codemap::{CodeMap, Span, Spanned}; - -use crate::{ - atrule::{ - keyframes::{Keyframes, KeyframesRuleSet}, - media::MediaRule, - mixin::Content, - AtRuleKind, SupportsRule, UnknownAtRule, - }, - builtin::modules::{ModuleConfig, Modules}, - error::SassResult, - lexer::Lexer, - scope::{Scope, Scopes}, - selector::{ - ComplexSelectorComponent, ExtendRule, ExtendedSelector, Extender, Selector, SelectorParser, - }, - style::Style, - utils::read_until_semicolon_or_closing_curly_brace, - value::Value, - Options, {Cow, Token}, -}; - -use common::{Comment, ContextFlags, NeverEmptyVec, SelectorOrStyle}; -pub(crate) use value::{HigherIntermediateValue, ValueVisitor}; -use variable::VariableValue; - -mod args; -pub mod common; -mod control_flow; -mod function; -mod ident; -mod import; +use crate::ast::*; + +pub(crate) use at_root_query::AtRootQueryParser; +pub(crate) use base::BaseParser; +pub(crate) use css::CssParser; +pub(crate) use keyframes::KeyframesSelectorParser; +pub(crate) use media_query::MediaQueryParser; +pub(crate) use sass::SassParser; +pub(crate) use scss::ScssParser; +pub(crate) use stylesheet::StylesheetParser; + +mod at_root_query; +mod base; +mod css; mod keyframes; -mod media; -mod mixin; -mod module; -mod style; -mod throw_away; +mod media_query; +mod sass; +mod scss; +mod stylesheet; mod value; -mod variable; #[derive(Debug, Clone)] -pub(crate) enum Stmt { - RuleSet { - selector: ExtendedSelector, - body: Vec, - }, - Style(Style), - Media(Box), - UnknownAtRule(Box), - Supports(Box), - AtRoot { - body: Vec, - }, - Comment(String), - Return(Box), - Keyframes(Box), - KeyframesRuleSet(Box), - /// A plain import such as `@import "foo.css";` or - /// `@import url(https://fonts.google.com/foo?bar);` - Import(String), -} - -// todo: merge at_root and at_root_has_selector into an enum -pub(crate) struct Parser<'a, 'b> { - pub toks: &'a mut Lexer<'b>, - pub map: &'a mut CodeMap, - pub path: &'a Path, - pub global_scope: &'a mut Scope, - pub scopes: &'a mut Scopes, - pub content_scopes: &'a mut Scopes, - pub super_selectors: &'a mut NeverEmptyVec, - pub span_before: Span, - pub content: &'a mut Vec, - pub flags: ContextFlags, - /// Whether this parser is at the root of the document - /// E.g. not inside a style, mixin, or function - pub at_root: bool, - /// If this parser is inside an `@at-rule` block, this is whether or - /// not the `@at-rule` block has a super selector - pub at_root_has_selector: bool, - pub extender: &'a mut Extender, - - pub options: &'a Options<'a>, - - pub modules: &'a mut Modules, - pub module_config: &'a mut ModuleConfig, -} - -impl<'a, 'b> Parser<'a, 'b> { - pub fn parse(&mut self) -> SassResult> { - let mut stmts = Vec::new(); - - // Allow a byte-order mark at the beginning of the document. - self.consume_char_if_exists('\u{feff}'); - - self.whitespace(); - stmts.append(&mut self.load_modules()?); - - while self.toks.peek().is_some() { - stmts.append(&mut self.parse_stmt()?); - if self.flags.in_function() && !stmts.is_empty() { - return Ok(stmts); - } - self.at_root = true; - } - - Ok(stmts) - } - - pub fn expect_char(&mut self, c: char) -> SassResult<()> { - match self.toks.peek() { - Some(Token { kind, pos }) if kind == c => { - self.span_before = pos; - self.toks.next(); - Ok(()) - } - Some(Token { pos, .. }) => Err((format!("expected \"{}\".", c), pos).into()), - None => Err((format!("expected \"{}\".", c), self.span_before).into()), - } - } - - pub fn consume_char_if_exists(&mut self, c: char) -> bool { - if let Some(Token { kind, .. }) = self.toks.peek() { - if kind == c { - self.toks.next(); - return true; - } - } - - false - } - - pub fn expect_identifier(&mut self, ident: &'static str) -> SassResult<()> { - let this_ident = self.parse_identifier_no_interpolation(false)?; - - self.span_before = this_ident.span; - - if this_ident.node == ident { - return Ok(()); - } - - Err((format!("Expected \"{}\".", ident), this_ident.span).into()) - } - - fn parse_stmt(&mut self) -> SassResult> { - let mut stmts = Vec::new(); - while let Some(Token { kind, pos }) = self.toks.peek() { - if self.flags.in_function() && !stmts.is_empty() { - return Ok(stmts); - } - self.span_before = pos; - match kind { - '@' => { - self.toks.next(); - let kind_string = self.parse_identifier()?; - self.span_before = kind_string.span; - match AtRuleKind::try_from(&kind_string)? { - AtRuleKind::Import => stmts.append(&mut self.import()?), - AtRuleKind::Mixin => self.parse_mixin()?, - AtRuleKind::Content => stmts.append(&mut self.parse_content_rule()?), - AtRuleKind::Include => stmts.append(&mut self.parse_include()?), - AtRuleKind::Function => self.parse_function()?, - AtRuleKind::Return => { - if self.flags.in_function() { - return Ok(vec![Stmt::Return(self.parse_return()?)]); - } - - return Err( - ("This at-rule is not allowed here.", kind_string.span).into() - ); - } - AtRuleKind::AtRoot => { - if self.flags.in_function() { - return Err(( - "This at-rule is not allowed here.", - kind_string.span, - ) - .into()); - } - - if self.at_root { - stmts.append(&mut self.parse_at_root()?); - } else { - let body = self.parse_at_root()?; - stmts.push(Stmt::AtRoot { body }); - } - } - AtRuleKind::Error => { - let Spanned { - node: message, - span, - } = self.parse_value(false, &|_| false)?; - - return Err(( - message.inspect(span)?.to_string(), - span.merge(kind_string.span), - ) - .into()); - } - AtRuleKind::Warn => { - let Spanned { - node: message, - span, - } = self.parse_value(false, &|_| false)?; - span.merge(kind_string.span); - - self.consume_char_if_exists(';'); - - self.warn(&Spanned { - node: message.to_css_string(span, false)?, - span, - }); - } - AtRuleKind::Debug => { - let Spanned { - node: message, - span, - } = self.parse_value(false, &|_| false)?; - span.merge(kind_string.span); - - self.consume_char_if_exists(';'); - - self.debug(&Spanned { - node: message.inspect(span)?, - span, - }); - } - AtRuleKind::If => stmts.append(&mut self.parse_if()?), - AtRuleKind::Each => stmts.append(&mut self.parse_each()?), - AtRuleKind::For => stmts.append(&mut self.parse_for()?), - AtRuleKind::While => stmts.append(&mut self.parse_while()?), - AtRuleKind::Charset => { - if self.flags.in_function() { - return Err(( - "This at-rule is not allowed here.", - kind_string.span, - ) - .into()); - } - - let val = self.parse_value(false, &|_| false)?; - - self.consume_char_if_exists(';'); - - if !val.node.is_quoted_string() { - return Err(("Expected string.", val.span).into()); - } - - continue; - } - AtRuleKind::Media => stmts.push(self.parse_media()?), - AtRuleKind::Unknown(_) => { - stmts.push(self.parse_unknown_at_rule(kind_string.node)?); - } - AtRuleKind::Use => { - return Err(( - "@use rules must be written before any other rules.", - kind_string.span, - ) - .into()) - } - AtRuleKind::Forward => todo!("@forward not yet implemented"), - AtRuleKind::Extend => self.parse_extend()?, - AtRuleKind::Supports => stmts.push(self.parse_supports()?), - AtRuleKind::Keyframes => { - stmts.push(self.parse_keyframes(kind_string.node)?); - } - } - } - '$' => self.parse_variable_declaration()?, - '\t' | '\n' | ' ' | ';' => { - self.toks.next(); - continue; - } - '/' => { - self.toks.next(); - let comment = self.parse_comment()?; - self.whitespace(); - match comment.node { - Comment::Silent => continue, - Comment::Loud(s) => { - if !self.flags.in_function() { - stmts.push(Stmt::Comment(s)); - } - } - } - } - '\u{0}'..='\u{8}' | '\u{b}'..='\u{1f}' => { - return Err(("expected selector.", pos).into()) - } - '}' => { - self.toks.next(); - break; - } - // dart-sass seems to special-case the error message here? - '!' | '{' => return Err(("expected \"}\".", pos).into()), - _ => { - if self.flags.in_function() { - return Err(( - "Functions can only contain variable declarations and control directives.", - self.span_before - ) - .into()); - } - if self.flags.in_keyframes() { - match self.is_selector_or_style()? { - SelectorOrStyle::ModuleVariableRedeclaration(module) => { - self.parse_module_variable_redeclaration(module)?; - } - SelectorOrStyle::Style(property, value) => { - if let Some(value) = value { - stmts.push(Stmt::Style(Style { property, value })); - } else { - stmts.extend( - self.parse_style_group(property)? - .into_iter() - .map(Stmt::Style), - ); - } - } - SelectorOrStyle::Selector(init) => { - let selector = self.parse_keyframes_selector(init)?; - self.scopes.enter_new_scope(); - - let body = self.parse_stmt()?; - self.scopes.exit_scope(); - stmts.push(Stmt::KeyframesRuleSet(Box::new(KeyframesRuleSet { - selector, - body, - }))); - } - } - continue; - } - - match self.is_selector_or_style()? { - SelectorOrStyle::ModuleVariableRedeclaration(module) => { - self.parse_module_variable_redeclaration(module)?; - } - SelectorOrStyle::Style(property, value) => { - if let Some(value) = value { - stmts.push(Stmt::Style(Style { property, value })); - } else { - stmts.extend( - self.parse_style_group(property)? - .into_iter() - .map(Stmt::Style), - ); - } - } - SelectorOrStyle::Selector(init) => { - let at_root = self.at_root; - self.at_root = false; - let selector = self - .parse_selector(true, false, init)? - .0 - .resolve_parent_selectors( - &self.super_selectors.last().clone().into_selector(), - !at_root || self.at_root_has_selector, - )?; - self.scopes.enter_new_scope(); - - let extended_selector = self.extender.add_selector(selector.0, None); - - self.super_selectors.push(extended_selector.clone()); - - let body = self.parse_stmt()?; - self.scopes.exit_scope(); - self.super_selectors.pop(); - self.at_root = self.super_selectors.is_empty(); - stmts.push(Stmt::RuleSet { - selector: extended_selector, - body, - }); - } - } - } - } - } - Ok(stmts) - } - - pub fn parse_selector( - &mut self, - allows_parent: bool, - from_fn: bool, - mut string: String, - ) -> SassResult<(Selector, bool)> { - let mut span = if let Some(tok) = self.toks.peek() { - tok.pos() - } else { - return Err(("expected \"{\".", self.span_before).into()); - }; - - self.span_before = span; - - let mut found_curly = false; - - let mut optional = false; - - // we resolve interpolation and strip comments - while let Some(Token { kind, pos }) = self.toks.next() { - span = span.merge(pos); - match kind { - '#' => { - if self.consume_char_if_exists('{') { - string.push_str( - &self - .parse_interpolation()? - .to_css_string(span, self.options.is_compressed())?, - ); - } else { - string.push('#'); - } - } - '/' => { - if self.toks.peek().is_none() { - return Err(("Expected selector.", pos).into()); - } - self.parse_comment()?; - string.push(' '); - } - '{' => { - if from_fn { - return Err(("Expected selector.", pos).into()); - } - - found_curly = true; - break; - } - '\\' => { - string.push('\\'); - if let Some(Token { kind, .. }) = self.toks.next() { - string.push(kind); - } - } - '!' => { - if from_fn { - self.expect_identifier("optional")?; - optional = true; - } else { - return Err(("expected \"{\".", pos).into()); - } - } - c => string.push(c), - } - } - - if !found_curly && !from_fn { - return Err(("expected \"{\".", span).into()); - } - - let sel_toks: Vec = string.chars().map(|x| Token::new(span, x)).collect(); - - let mut lexer = Lexer::new(sel_toks); - - let selector = SelectorParser::new( - &mut Parser { - toks: &mut lexer, - 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, - }, - allows_parent, - true, - span, - ) - .parse()?; - - Ok((Selector(selector), optional)) - } - - /// Eat and return the contents of a comment. - /// - /// This function assumes that the starting "/" has already been consumed - /// The entirety of the comment, including the ending "*/" for multiline comments, - /// is consumed. Note that the ending "*/" is not included in the output. - #[allow(clippy::eval_order_dependence)] - pub fn parse_comment(&mut self) -> SassResult> { - let mut span = self.span_before; - Ok(Spanned { - node: match self.toks.next() { - Some(Token { kind: '/', .. }) => { - while let Some(tok) = self.toks.peek() { - if tok.kind == '\n' { - break; - } - span = span.merge(tok.pos); - self.toks.next(); - } - - Comment::Silent - } - Some(Token { kind: '*', .. }) => { - let mut comment = String::new(); - while let Some(tok) = self.toks.next() { - span = span.merge(tok.pos()); - match (tok.kind, self.toks.peek()) { - ('*', Some(Token { kind: '/', .. })) => { - self.toks.next(); - break; - } - ('#', Some(Token { kind: '{', .. })) => { - self.toks.next(); - comment.push_str( - &self - .parse_interpolation()? - .to_css_string(span, self.options.is_compressed())?, - ); - continue; - } - (..) => comment.push(tok.kind), - } - } - Comment::Loud(comment) - } - Some(..) | None => return Err(("expected selector.", self.span_before).into()), - }, - span, - }) - } - - pub fn parse_interpolation(&mut self) -> SassResult> { - let val = self.parse_value(true, &|_| false)?; - - self.span_before = val.span; - - self.expect_char('}')?; - - Ok(val.map_node(Value::unquote)) - } - - pub fn parse_interpolation_as_string(&mut self) -> SassResult> { - let interpolation = self.parse_interpolation()?; - Ok(match interpolation.node { - Value::String(v, ..) => Cow::owned(v), - v => v.to_css_string(interpolation.span, self.options.is_compressed())?, - }) - } - - // todo: this should also consume silent comments - pub fn whitespace(&mut self) -> bool { - let mut found_whitespace = false; - while let Some(tok) = self.toks.peek() { - match tok.kind { - ' ' | '\t' | '\n' => { - self.toks.next(); - found_whitespace = true; - } - _ => return found_whitespace, - } - } - found_whitespace - } - - /// Eat tokens until a newline - /// - /// This exists largely to eat silent comments, "//" - /// We only have to check for \n as the lexing step normalizes all newline characters - /// - /// The newline is consumed - pub fn read_until_newline(&mut self) { - for tok in &mut self.toks { - if tok.kind == '\n' { - break; - } - } - } - - fn whitespace_or_comment(&mut self) -> bool { - let mut found_whitespace = false; - while let Some(tok) = self.toks.peek() { - match tok.kind { - ' ' | '\t' | '\n' => { - self.toks.next(); - found_whitespace = true; - } - '/' => match self.toks.peek_forward(1) { - Some(Token { kind: '*', .. }) => { - found_whitespace = true; - self.toks.next(); - self.toks.next(); - #[allow(clippy::while_let_on_iterator)] - while let Some(tok) = self.toks.next() { - if tok.kind == '*' && self.consume_char_if_exists('/') { - break; - } - } - } - Some(Token { kind: '/', .. }) => { - found_whitespace = true; - self.read_until_newline(); - } - _ => { - self.toks.reset_cursor(); - return found_whitespace; - } - }, - _ => return found_whitespace, - } - } - found_whitespace - } -} - -impl<'a, 'b> Parser<'a, 'b> { - fn parse_unknown_at_rule(&mut self, name: String) -> SassResult { - if self.flags.in_function() { - return Err(("This at-rule is not allowed here.", self.span_before).into()); - } - - let mut params = String::new(); - self.whitespace_or_comment(); - - loop { - match self.toks.peek() { - Some(Token { kind: '{', .. }) => { - self.toks.next(); - break; - } - Some(Token { kind: ';', .. }) | Some(Token { kind: '}', .. }) | None => { - self.consume_char_if_exists(';'); - return Ok(Stmt::UnknownAtRule(Box::new(UnknownAtRule { - name, - super_selector: Selector::new(self.span_before), - has_body: false, - params: params.trim().to_owned(), - body: Vec::new(), - }))); - } - Some(Token { kind: '#', .. }) => { - self.toks.next(); - - if let Some(Token { kind: '{', pos }) = self.toks.peek() { - self.span_before = self.span_before.merge(pos); - self.toks.next(); - params.push_str(&self.parse_interpolation_as_string()?); - } else { - params.push('#'); - } - continue; - } - Some(Token { kind: '\n', .. }) - | Some(Token { kind: ' ', .. }) - | Some(Token { kind: '\t', .. }) => { - self.whitespace(); - params.push(' '); - continue; - } - Some(Token { kind, .. }) => { - self.toks.next(); - params.push(kind); - } - } - } - - let raw_body = self.parse_stmt()?; - let mut rules = Vec::with_capacity(raw_body.len()); - let mut body = Vec::new(); - - for stmt in raw_body { - match stmt { - Stmt::Style(..) => body.push(stmt), - _ => rules.push(stmt), - } - } - - if !self.super_selectors.last().as_selector_list().is_empty() { - body = vec![Stmt::RuleSet { - selector: self.super_selectors.last().clone(), - body, - }]; - } - - body.append(&mut rules); - - Ok(Stmt::UnknownAtRule(Box::new(UnknownAtRule { - name, - super_selector: Selector::new(self.span_before), - params: params.trim().to_owned(), - has_body: true, - body, - }))) - } - - fn parse_media(&mut self) -> SassResult { - if self.flags.in_function() { - return Err(("This at-rule is not allowed here.", self.span_before).into()); - } - - let query = self.parse_media_query_list()?; - - self.whitespace(); - - self.expect_char('{')?; - - let raw_body = self.parse_stmt()?; - - let mut rules = Vec::with_capacity(raw_body.len()); - let mut body = Vec::new(); - - for stmt in raw_body { - match stmt { - Stmt::Style(..) => body.push(stmt), - _ => rules.push(stmt), - } - } - - if !self.super_selectors.last().as_selector_list().is_empty() { - body = vec![Stmt::RuleSet { - selector: self.super_selectors.last().clone(), - body, - }]; - } - - body.append(&mut rules); - - Ok(Stmt::Media(Box::new(MediaRule { - super_selector: Selector::new(self.span_before), - query, - body, - }))) - } - - fn parse_at_root(&mut self) -> SassResult> { - self.whitespace(); - let mut at_root_has_selector = false; - let at_rule_selector = if self.consume_char_if_exists('{') { - self.super_selectors.last().clone() - } else { - at_root_has_selector = true; - let selector = self - .parse_selector(true, false, String::new())? - .0 - .resolve_parent_selectors( - &self.super_selectors.last().clone().into_selector(), - false, - )?; - - self.extender.add_selector(selector.0, None) - }; - - self.whitespace(); - - let mut styles = Vec::new(); - #[allow(clippy::unnecessary_filter_map)] - let raw_stmts = Parser { - toks: self.toks, - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: &mut NeverEmptyVec::new(at_rule_selector.clone()), - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_AT_ROOT_RULE, - at_root: true, - 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()? - .into_iter() - .filter_map(|s| match s { - Stmt::Style(..) => { - styles.push(s); - None - } - _ => Some(Ok(s)), - }) - .collect::>>()?; - - let stmts = if at_root_has_selector { - let mut body = styles; - body.extend(raw_stmts); - - vec![Stmt::RuleSet { - body, - selector: at_rule_selector, - }] - } else { - if !styles.is_empty() { - return Err(( - "Found style at the toplevel inside @at-root.", - self.span_before, - ) - .into()); - } - - raw_stmts - }; - - Ok(stmts) - } - - fn parse_extend(&mut self) -> SassResult<()> { - if self.flags.in_function() { - return Err(("This at-rule is not allowed here.", self.span_before).into()); - } - // todo: track when inside ruleset or `@content` - // if !self.in_style_rule && !self.in_mixin && !self.in_content_block { - // return Err(("@extend may only be used within style rules.", self.span_before).into()); - // } - let (value, is_optional) = Parser { - toks: &mut Lexer::new(read_until_semicolon_or_closing_curly_brace(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, - 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_selector(false, true, String::new())?; - - // todo: this might be superfluous - self.whitespace(); - - self.consume_char_if_exists(';'); - - let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before); - - let super_selector = self.super_selectors.last(); - - for complex in value.0.components { - if complex.components.len() != 1 || !complex.components.first().unwrap().is_compound() { - // If the selector was a compound selector but not a simple - // selector, emit a more explicit error. - return Err(("complex selectors may not be extended.", self.span_before).into()); - } - - let compound = match complex.components.first() { - Some(ComplexSelectorComponent::Compound(c)) => c, - Some(..) | None => todo!(), - }; - if compound.components.len() != 1 { - return Err(( - format!( - "compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n", - compound.components.iter().map(ToString::to_string).collect::>().join(", ") - ) - , self.span_before).into()); - } - - self.extender.add_extension( - super_selector.clone().into_selector().0, - compound.components.first().unwrap(), - &extend_rule, - &None, - self.span_before, - ); - } - - Ok(()) - } - - fn parse_supports(&mut self) -> SassResult { - if self.flags.in_function() { - return Err(("This at-rule is not allowed here.", self.span_before).into()); - } - - let params = self.parse_media_args()?; - - if params.is_empty() { - return Err(("Expected \"not\".", self.span_before).into()); - } - - let raw_body = self.parse_stmt()?; - - let mut rules = Vec::with_capacity(raw_body.len()); - let mut body = Vec::new(); - - for stmt in raw_body { - match stmt { - Stmt::Style(..) => body.push(stmt), - _ => rules.push(stmt), - } - } - - if !self.super_selectors.last().as_selector_list().is_empty() { - body = vec![Stmt::RuleSet { - selector: self.super_selectors.last().clone(), - body, - }]; - } - - body.append(&mut rules); - - Ok(Stmt::Supports(Box::new(SupportsRule { - params: params.trim().to_owned(), - body, - }))) - } - - // todo: we should use a specialized struct to represent these - fn parse_media_args(&mut self) -> SassResult { - let mut params = String::new(); - self.whitespace(); - while let Some(tok) = self.toks.next() { - match tok.kind { - '{' => break, - '#' => { - if let Some(Token { kind: '{', pos }) = self.toks.peek() { - self.toks.next(); - self.span_before = pos; - let interpolation = self.parse_interpolation()?; - params.push_str( - &interpolation - .node - .to_css_string(interpolation.span, self.options.is_compressed())?, - ); - continue; - } - - params.push(tok.kind); - } - '\n' | ' ' | '\t' => { - self.whitespace(); - params.push(' '); - continue; - } - _ => {} - } - params.push(tok.kind); - } - Ok(params) - } +pub(crate) enum DeclarationOrBuffer { + Stmt(AstStmt), + Buffer(Interpolation), } -impl<'a, 'b> Parser<'a, 'b> { - fn debug(&self, message: &Spanned>) { - if self.options.quiet { - return; - } - let loc = self.map.look_up_span(message.span); - eprintln!( - "{}:{} DEBUG: {}", - loc.file.name(), - loc.begin.line + 1, - message.node - ); - } +/// Names that functions are not allowed to have +pub(super) const RESERVED_IDENTIFIERS: [&str; 8] = [ + "calc", + "element", + "expression", + "url", + "and", + "or", + "not", + "clamp", +]; - fn warn(&self, message: &Spanned>) { - if self.options.quiet { - return; - } - let loc = self.map.look_up_span(message.span); - eprintln!( - "Warning: {}\n {} {}:{} root stylesheet", - message.node, - loc.file.name(), - loc.begin.line + 1, - loc.begin.column + 1 - ); - } +#[derive(Debug, Clone)] +pub(crate) enum VariableDeclOrInterpolation { + VariableDecl(AstVariableDecl), + Interpolation(Interpolation), } diff --git a/src/parse/module.rs b/src/parse/module.rs deleted file mode 100644 index 00674ee2..00000000 --- a/src/parse/module.rs +++ /dev/null @@ -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> { - 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 { - 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)> { - 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> { - 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(()) - } -} diff --git a/src/parse/sass.rs b/src/parse/sass.rs new file mode 100644 index 00000000..88ae30c6 --- /dev/null +++ b/src/parse/sass.rs @@ -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, + pub spaces: Option, + pub next_indentation_end: Option, +} + +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 { + 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 { + Ok(self.at_end_of_statement() && self.peek_indentation()? > self.current_indentation) + } + + fn scan_else(&mut self, if_indentation: usize) -> SassResult { + 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, + ) -> SassResult> { + 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>, + ) -> SassResult> { + 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 { + 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 { + 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 { + 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 { + 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>, + ) -> SassResult> { + 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, + } + } +} diff --git a/src/parse/scss.rs b/src/parse/scss.rs new file mode 100644 index 00000000..6be08cbd --- /dev/null +++ b/src/parse/scss.rs @@ -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 + } +} diff --git a/src/parse/style.rs b/src/parse/style.rs deleted file mode 100644 index 451831a5..00000000 --- a/src/parse/style.rs +++ /dev/null @@ -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> { - 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 { - 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 { - 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> { - self.parse_value(false, &|_| false) - } - - pub(super) fn parse_style_group( - &mut self, - super_property: InternedString, - ) -> SassResult> { - 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) - } -} diff --git a/src/parse/stylesheet.rs b/src/parse/stylesheet.rs new file mode 100644 index 00000000..b11b5d85 --- /dev/null +++ b/src/parse/stylesheet.rs @@ -0,0 +1,3058 @@ +use std::{ + cell::Cell, + collections::{BTreeMap, HashSet}, + ffi::OsString, + mem, + path::{Path, PathBuf}, +}; + +use codemap::{CodeMap, Span, Spanned}; + +use crate::{ + ast::*, + common::{unvendor, Identifier, QuoteKind}, + error::SassResult, + lexer::Lexer, + utils::{is_name, is_name_start, is_plain_css_import, opposite_bracket}, + ContextFlags, Options, Token, +}; + +use super::{ + value::{Predicate, ValueParser}, + BaseParser, DeclarationOrBuffer, ScssParser, VariableDeclOrInterpolation, RESERVED_IDENTIFIERS, +}; + +// todo: can we simplify lifetimes (by maybe not storing reference to lexer) +/// Default implementations are oriented towards the SCSS syntax, as both CSS and +/// SCSS share the behavior +pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized { + // todo: make constant? + fn is_plain_css(&mut self) -> bool; + // todo: make constant? + fn is_indented(&mut self) -> bool; + fn options(&self) -> &Options; + fn path(&mut self) -> &'a Path; + fn map(&mut self) -> &mut CodeMap; + fn span_before(&self) -> Span; + fn current_indentation(&self) -> usize; + fn flags(&mut self) -> &ContextFlags; + fn flags_mut(&mut self) -> &mut ContextFlags; + + #[allow(clippy::type_complexity)] + const IDENTIFIER_LIKE: Option SassResult>> = None; + + fn parse_style_rule_selector(&mut self) -> SassResult { + self.almost_any_value(false) + } + + fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> { + self.whitespace_without_comments(); + match self.toks().peek() { + Some(Token { + kind: ';' | '}', .. + }) + | None => Ok(()), + _ => { + self.expect_char(';')?; + Ok(()) + } + } + } + + fn at_end_of_statement(&self) -> bool { + matches!( + self.toks().peek(), + Some(Token { + kind: ';' | '}' | '{', + .. + }) | None + ) + } + + fn looking_at_children(&mut self) -> SassResult { + Ok(matches!(self.toks().peek(), Some(Token { kind: '{', .. }))) + } + + fn scan_else(&mut self, _if_indentation: usize) -> SassResult { + let start = self.toks().cursor(); + + self.whitespace()?; + + if self.scan_char('@') { + if self.scan_identifier("else", true)? { + return Ok(true); + } + + if self.scan_identifier("elseif", true)? { + // todo: deprecation warning here + let new_cursor = self.toks().cursor() - 2; + self.toks_mut().set_cursor(new_cursor); + return Ok(true); + } + } + + self.toks_mut().set_cursor(start); + + Ok(false) + } + + fn parse_children( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult> { + self.expect_char('{')?; + self.whitespace_without_comments(); + let mut children = Vec::new(); + + let mut found_matching_brace = false; + + while let Some(tok) = self.toks().peek() { + match tok.kind { + '$' => children.push(AstStmt::VariableDecl( + self.parse_variable_declaration_without_namespace(None, None)?, + )), + '/' => match self.toks().peek_n(1) { + Some(Token { kind: '/', .. }) => { + children.push(self.parse_silent_comment()?); + self.whitespace_without_comments(); + } + Some(Token { kind: '*', .. }) => { + children.push(AstStmt::LoudComment(self.parse_loud_comment()?)); + self.whitespace_without_comments(); + } + _ => children.push(child(self)?), + }, + ';' => { + self.toks_mut().next(); + self.whitespace_without_comments(); + } + '}' => { + self.expect_char('}')?; + found_matching_brace = true; + break; + } + _ => children.push(child(self)?), + } + } + + if !found_matching_brace { + return Err(("expected \"}\".", self.toks().current_span()).into()); + } + + Ok(children) + } + + fn parse_statements( + &mut self, + statement: fn(&mut Self) -> SassResult>, + ) -> SassResult> { + let mut stmts = Vec::new(); + self.whitespace_without_comments(); + while let Some(tok) = self.toks().peek() { + match tok.kind { + '$' => stmts.push(AstStmt::VariableDecl( + self.parse_variable_declaration_without_namespace(None, None)?, + )), + '/' => match self.toks().peek_n(1) { + Some(Token { kind: '/', .. }) => { + stmts.push(self.parse_silent_comment()?); + self.whitespace_without_comments(); + } + Some(Token { kind: '*', .. }) => { + stmts.push(AstStmt::LoudComment(self.parse_loud_comment()?)); + self.whitespace_without_comments(); + } + _ => { + if let Some(stmt) = statement(self)? { + stmts.push(stmt); + } + } + }, + ';' => { + self.toks_mut().next(); + self.whitespace_without_comments(); + } + _ => { + if let Some(stmt) = statement(self)? { + stmts.push(stmt); + } + } + } + } + + Ok(stmts) + } + + fn __parse(&mut self) -> SassResult { + let mut style_sheet = StyleSheet::new(self.is_plain_css(), self.path().to_path_buf()); + + // Allow a byte-order mark at the beginning of the document. + self.scan_char('\u{feff}'); + + style_sheet.body = self.parse_statements(|parser| { + if parser.next_matches("@charset") { + parser.expect_char('@')?; + parser.expect_identifier("charset", false)?; + parser.whitespace()?; + parser.parse_string()?; + return Ok(None); + } + + Ok(Some(parser.parse_statement()?)) + })?; + + Ok(style_sheet) + } + + fn looking_at_expression(&mut self) -> bool { + let character = if let Some(c) = self.toks().peek() { + c + } else { + return false; + }; + + match character.kind { + '.' => !matches!(self.toks().peek_n(1), Some(Token { kind: '.', .. })), + '!' => match self.toks().peek_n(1) { + Some(Token { + kind: 'i' | 'I', .. + }) + | None => true, + Some(Token { kind, .. }) => kind.is_ascii_whitespace(), + }, + '(' | '/' | '[' | '\'' | '"' | '#' | '+' | '-' | '\\' | '$' | '&' => true, + c => is_name_start(c) || c.is_ascii_digit(), + } + } + + fn parse_argument_declaration(&mut self) -> SassResult { + self.expect_char('(')?; + self.whitespace()?; + + let mut arguments = Vec::new(); + let mut named = HashSet::new(); + + let mut rest_argument: Option = None; + + while self.toks_mut().next_char_is('$') { + let name_start = self.toks().cursor(); + let name = Identifier::from(self.parse_variable_name()?); + let name_span = self.toks_mut().span_from(name_start); + self.whitespace()?; + + let mut default_value: Option = None; + + if self.scan_char(':') { + self.whitespace()?; + default_value = Some(self.parse_expression_until_comma(false)?.node); + } else if self.scan_char('.') { + self.expect_char('.')?; + self.expect_char('.')?; + self.whitespace()?; + rest_argument = Some(name); + break; + } + + arguments.push(Argument { + name, + default: default_value, + }); + + if !named.insert(name) { + return Err(("Duplicate argument.", name_span).into()); + } + + if !self.scan_char(',') { + break; + } + self.whitespace()?; + } + self.expect_char(')')?; + + Ok(ArgumentDeclaration { + args: arguments, + rest: rest_argument, + }) + } + + fn plain_at_rule_name(&mut self) -> SassResult { + self.expect_char('@')?; + let name = self.parse_identifier(false, false)?; + self.whitespace()?; + Ok(name) + } + + fn with_children( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult>> { + let start = self.toks().cursor(); + let children = self.parse_children(child)?; + let span = self.toks_mut().span_from(start); + self.whitespace_without_comments(); + Ok(Spanned { + node: children, + span, + }) + } + + fn parse_at_root_query(&mut self) -> SassResult { + if self.toks_mut().next_char_is('#') { + return self.parse_single_interpolation(); + } + + let mut buffer = Interpolation::new(); + self.expect_char('(')?; + buffer.add_char('('); + + self.whitespace()?; + + buffer.add_expr(self.parse_expression(None, None, None)?); + + if self.scan_char(':') { + self.whitespace()?; + buffer.add_char(':'); + buffer.add_char(' '); + buffer.add_expr(self.parse_expression(None, None, None)?); + } + + self.expect_char(')')?; + self.whitespace()?; + buffer.add_char(')'); + + Ok(buffer) + } + + fn parse_at_root_rule(&mut self, start: usize) -> SassResult { + Ok(AstStmt::AtRootRule(if self.toks_mut().next_char_is('(') { + let query = self.parse_at_root_query()?; + self.whitespace()?; + let children = self.with_children(Self::parse_statement)?.node; + + AstAtRootRule { + query: Some(query), + children, + span: self.toks_mut().span_from(start), + } + } else if self.looking_at_children()? { + let children = self.with_children(Self::parse_statement)?.node; + AstAtRootRule { + query: None, + children, + span: self.toks_mut().span_from(start), + } + } else { + let child = self.parse_style_rule(None, None)?; + AstAtRootRule { + query: None, + children: vec![child], + span: self.toks_mut().span_from(start), + } + })) + } + + fn parse_content_rule(&mut self, start: usize) -> SassResult { + if !self.flags().in_mixin() { + return Err(( + "@content is only allowed within mixin declarations.", + self.toks_mut().span_from(start), + ) + .into()); + } + + self.whitespace()?; + + let args = if self.toks_mut().next_char_is('(') { + self.parse_argument_invocation(true, false)? + } else { + ArgumentInvocation::empty(self.toks().current_span()) + }; + + self.expect_statement_separator(Some("@content rule"))?; + + self.flags_mut().set(ContextFlags::FOUND_CONTENT_RULE, true); + + Ok(AstStmt::ContentRule(AstContentRule { args })) + } + + fn parse_debug_rule(&mut self) -> SassResult { + let value = self.parse_expression(None, None, None)?; + self.expect_statement_separator(Some("@debug rule"))?; + + Ok(AstStmt::Debug(AstDebugRule { + value: value.node, + span: value.span, + })) + } + + fn parse_each_rule( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult { + let was_in_control_directive = self.flags().in_control_flow(); + self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); + + let mut variables = vec![Identifier::from(self.parse_variable_name()?)]; + self.whitespace()?; + while self.scan_char(',') { + self.whitespace()?; + variables.push(Identifier::from(self.parse_variable_name()?)); + self.whitespace()?; + } + + self.expect_identifier("in", false)?; + self.whitespace()?; + + let list = self.parse_expression(None, None, None)?.node; + + let body = self.with_children(child)?.node; + + self.flags_mut() + .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); + + Ok(AstStmt::Each(AstEach { + variables, + list, + body, + })) + } + + fn parse_disallowed_at_rule(&mut self, start: usize) -> SassResult { + self.almost_any_value(false)?; + Err(( + "This at-rule is not allowed here.", + self.toks_mut().span_from(start), + ) + .into()) + } + + fn parse_error_rule(&mut self) -> SassResult { + let value = self.parse_expression(None, None, None)?; + self.expect_statement_separator(Some("@error rule"))?; + Ok(AstStmt::ErrorRule(AstErrorRule { + value: value.node, + span: value.span, + })) + } + + fn parse_extend_rule(&mut self, start: usize) -> SassResult { + if !self.flags().in_style_rule() + && !self.flags().in_mixin() + && !self.flags().in_content_block() + { + return Err(( + "@extend may only be used within style rules.", + self.toks_mut().span_from(start), + ) + .into()); + } + + let value = self.almost_any_value(false)?; + + let is_optional = self.scan_char('!'); + + if is_optional { + self.expect_identifier("optional", false)?; + } + + self.expect_statement_separator(Some("@extend rule"))?; + + Ok(AstStmt::Extend(AstExtendRule { + value, + is_optional, + span: self.toks_mut().span_from(start), + })) + } + + fn parse_for_rule( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult { + let was_in_control_directive = self.flags().in_control_flow(); + self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); + + let var_start = self.toks().cursor(); + let variable = Spanned { + node: Identifier::from(self.parse_variable_name()?), + span: self.toks_mut().span_from(var_start), + }; + self.whitespace()?; + + self.expect_identifier("from", false)?; + self.whitespace()?; + + let exclusive: Cell> = Cell::new(None); + + let from = self.parse_expression( + Some(&|parser| { + if !parser.looking_at_identifier() { + return Ok(false); + } + Ok(if parser.scan_identifier("to", false)? { + exclusive.set(Some(true)); + true + } else if parser.scan_identifier("through", false)? { + exclusive.set(Some(false)); + true + } else { + false + }) + }), + None, + None, + )?; + + let is_exclusive = match exclusive.get() { + Some(b) => b, + None => { + return Err(( + "Expected \"to\" or \"through\".", + self.toks().current_span(), + ) + .into()) + } + }; + + self.whitespace()?; + + let to = self.parse_expression(None, None, None)?; + + let body = self.with_children(child)?.node; + + self.flags_mut() + .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); + + Ok(AstStmt::For(AstFor { + variable, + from, + to, + is_exclusive, + body, + })) + } + + fn parse_function_rule(&mut self, start: usize) -> SassResult { + let name_start = self.toks().cursor(); + let name = self.parse_identifier(true, false)?; + let name_span = self.toks_mut().span_from(name_start); + self.whitespace()?; + let arguments = self.parse_argument_declaration()?; + + if self.flags().in_mixin() || self.flags().in_content_block() { + return Err(( + "Mixins may not contain function declarations.", + self.toks_mut().span_from(start), + ) + .into()); + } else if self.flags().in_control_flow() { + return Err(( + "Functions may not be declared in control directives.", + self.toks_mut().span_from(start), + ) + .into()); + } + + if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) { + return Err(("Invalid function name.", self.toks_mut().span_from(start)).into()); + } + + self.whitespace()?; + + let children = self.with_children(Self::function_child)?.node; + + Ok(AstStmt::FunctionDecl(AstFunctionDecl { + name: Spanned { + node: Identifier::from(name), + span: name_span, + }, + arguments, + children, + })) + } + + fn parse_variable_declaration_with_namespace(&mut self) -> SassResult { + let start = self.toks().cursor(); + let namespace = self.parse_identifier(false, false)?; + let namespace_span = self.toks_mut().span_from(start); + self.expect_char('.')?; + self.parse_variable_declaration_without_namespace( + Some(Spanned { + node: Identifier::from(namespace), + span: namespace_span, + }), + Some(start), + ) + } + + fn function_child(&mut self) -> SassResult { + let start = self.toks().cursor(); + if !self.toks_mut().next_char_is('@') { + match self.parse_variable_declaration_with_namespace() { + Ok(decl) => return Ok(AstStmt::VariableDecl(decl)), + Err(e) => { + self.toks_mut().set_cursor(start); + let stmt = match self.parse_declaration_or_style_rule() { + Ok(stmt) => stmt, + Err(..) => return Err(e), + }; + + let (is_style_rule, span) = match stmt { + AstStmt::RuleSet(ruleset) => (true, ruleset.span), + AstStmt::Style(style) => (false, style.span), + _ => unreachable!(), + }; + + return Err(( + format!( + "@function rules may not contain {}.", + if is_style_rule { + "style rules" + } else { + "declarations" + } + ), + span, + ) + .into()); + } + } + } + + return match self.plain_at_rule_name()?.as_str() { + "debug" => self.parse_debug_rule(), + "each" => self.parse_each_rule(Self::function_child), + "else" => self.parse_disallowed_at_rule(start), + "error" => self.parse_error_rule(), + "for" => self.parse_for_rule(Self::function_child), + "if" => self.parse_if_rule(Self::function_child), + "return" => self.parse_return_rule(), + "warn" => self.parse_warn_rule(), + "while" => self.parse_while_rule(Self::function_child), + _ => self.parse_disallowed_at_rule(start), + }; + } + + fn parse_if_rule( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult { + let if_indentation = self.current_indentation(); + + let was_in_control_directive = self.flags().in_control_flow(); + self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); + let condition = self.parse_expression(None, None, None)?.node; + let body = self.parse_children(child)?; + self.whitespace_without_comments(); + + let mut clauses = vec![AstIfClause { condition, body }]; + + let mut last_clause: Option> = None; + + while self.scan_else(if_indentation)? { + self.whitespace()?; + if self.scan_identifier("if", false)? { + self.whitespace()?; + let condition = self.parse_expression(None, None, None)?.node; + let body = self.parse_children(child)?; + clauses.push(AstIfClause { condition, body }); + } else { + last_clause = Some(self.parse_children(child)?); + break; + } + } + + self.flags_mut() + .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); + self.whitespace_without_comments(); + + Ok(AstStmt::If(AstIf { + if_clauses: clauses, + else_clause: last_clause, + })) + } + + fn try_parse_import_supports_function(&mut self) -> SassResult> { + if !self.looking_at_interpolated_identifier() { + return Ok(None); + } + + let start = self.toks().cursor(); + let name = self.parse_interpolated_identifier()?; + debug_assert!(name.as_plain() != Some("not")); + + if !self.scan_char('(') { + self.toks_mut().set_cursor(start); + return Ok(None); + } + + let value = self.parse_interpolated_declaration_value(true, true, true)?; + self.expect_char(')')?; + + Ok(Some(AstSupportsCondition::Function { name, args: value })) + } + + fn parse_import_supports_query(&mut self) -> SassResult { + Ok(if self.scan_identifier("not", false)? { + self.whitespace()?; + AstSupportsCondition::Negation(Box::new(self.supports_condition_in_parens()?)) + } else if self.toks_mut().next_char_is('(') { + self.parse_supports_condition()? + } else { + match self.try_parse_import_supports_function()? { + Some(function) => function, + None => { + let start = self.toks().cursor(); + let name = self.parse_expression(None, None, None)?; + self.expect_char(':')?; + self.supports_declaration_value(name.node, start)? + } + } + }) + } + + fn try_import_modifiers(&mut self) -> SassResult> { + // Exit before allocating anything if we're not looking at any modifiers, as + // is the most common case. + if !self.looking_at_interpolated_identifier() && !self.toks_mut().next_char_is('(') { + return Ok(None); + } + + let mut buffer = Interpolation::new(); + + loop { + if self.looking_at_interpolated_identifier() { + if !buffer.is_empty() { + buffer.add_char(' '); + } + + let identifier = self.parse_interpolated_identifier()?; + let name = identifier.as_plain().map(str::to_ascii_lowercase); + buffer.add_interpolation(identifier); + + if name.as_deref() != Some("and") && self.scan_char('(') { + if name.as_deref() == Some("supports") { + let query = self.parse_import_supports_query()?; + let is_declaration = + matches!(query, AstSupportsCondition::Declaration { .. }); + + if !is_declaration { + buffer.add_char('('); + } + + buffer + .add_expr(AstExpr::Supports(Box::new(query)).span(self.span_before())); + + if !is_declaration { + buffer.add_char(')'); + } + } else { + buffer.add_char('('); + buffer.add_interpolation( + self.parse_interpolated_declaration_value(true, true, true)?, + ); + buffer.add_char(')'); + } + + self.expect_char(')')?; + self.whitespace()?; + } else { + self.whitespace()?; + if self.scan_char(',') { + buffer.add_char(','); + buffer.add_char(' '); + buffer.add_interpolation(self.parse_media_query_list()?); + return Ok(Some(buffer)); + } + } + } else if self.toks_mut().next_char_is('(') { + if !buffer.is_empty() { + buffer.add_char(' '); + } + + buffer.add_interpolation(self.parse_media_query_list()?); + return Ok(Some(buffer)); + } else { + return Ok(Some(buffer)); + } + } + } + + fn try_url_contents(&mut self, name: Option<&str>) -> SassResult> { + let start = self.toks().cursor(); + if !self.scan_char('(') { + return Ok(None); + } + self.whitespace_without_comments(); + + // 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 = Interpolation::new(); + buffer.add_string(name.unwrap_or("url").to_owned()); + buffer.add_char('('); + + while let Some(next) = self.toks().peek() { + match next.kind { + '\\' => buffer.add_string(self.parse_escape(false)?), + '!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { + self.toks_mut().next(); + buffer.add_char(next.kind); + } + '#' => { + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { + let interpolation = self.parse_single_interpolation()?; + buffer.add_interpolation(interpolation); + } else { + self.toks_mut().next(); + buffer.add_char(next.kind); + } + } + ')' => { + self.toks_mut().next(); + buffer.add_char(next.kind); + return Ok(Some(buffer)); + } + ' ' | '\t' | '\n' | '\r' => { + self.whitespace_without_comments(); + if !self.toks_mut().next_char_is(')') { + break; + } + } + _ => break, + } + } + + self.toks_mut().set_cursor(start); + + Ok(None) + } + + fn parse_dynamic_url(&mut self) -> SassResult { + let start = self.toks().cursor(); + self.expect_identifier("url", false)?; + + Ok(match self.try_url_contents(None)? { + Some(contents) => AstExpr::String( + StringExpr(contents, QuoteKind::None), + self.toks_mut().span_from(start), + ), + None => AstExpr::InterpolatedFunction(InterpolatedFunction { + name: Interpolation::new_plain("url".to_owned()), + arguments: Box::new(self.parse_argument_invocation(false, false)?), + span: self.toks_mut().span_from(start), + }), + }) + } + + fn parse_import_argument(&mut self, start: usize) -> SassResult { + if self.toks_mut().next_char_is('u') || self.toks_mut().next_char_is('U') { + let url = self.parse_dynamic_url()?; + self.whitespace()?; + let modifiers = self.try_import_modifiers()?; + return Ok(AstImport::Plain(AstPlainCssImport { + url: Interpolation::new_with_expr(url.span(self.toks_mut().span_from(start))), + modifiers, + span: self.toks_mut().span_from(start), + })); + } + + let start = self.toks().cursor(); + let url = self.parse_string()?; + let raw_url = self.toks().raw_text(start); + self.whitespace()?; + let modifiers = self.try_import_modifiers()?; + + let span = self.toks_mut().span_from(start); + + if is_plain_css_import(&url) || modifiers.is_some() { + Ok(AstImport::Plain(AstPlainCssImport { + url: Interpolation::new_plain(raw_url), + modifiers, + span, + })) + } else { + // todo: try parseImportUrl + Ok(AstImport::Sass(AstSassImport { url, span })) + } + } + + fn parse_import_rule(&mut self, start: usize) -> SassResult { + let mut imports = Vec::new(); + + loop { + self.whitespace()?; + let argument = self.parse_import_argument(self.toks().cursor())?; + + // todo: _inControlDirective + if (self.flags().in_control_flow() || self.flags().in_mixin()) && argument.is_dynamic() + { + self.parse_disallowed_at_rule(start)?; + } + + imports.push(argument); + self.whitespace()?; + + if !self.scan_char(',') { + break; + } + } + + Ok(AstStmt::ImportRule(AstImportRule { imports })) + } + + fn parse_public_identifier(&mut self) -> SassResult { + let start = self.toks().cursor(); + let ident = self.parse_identifier(true, false)?; + Self::assert_public(&ident, self.toks_mut().span_from(start))?; + + Ok(ident) + } + + fn parse_include_rule(&mut self) -> SassResult { + let mut namespace: Option> = None; + + let name_start = self.toks().cursor(); + let mut name = self.parse_identifier(false, false)?; + + if self.scan_char('.') { + let namespace_span = self.toks_mut().span_from(name_start); + namespace = Some(Spanned { + node: Identifier::from(name), + span: namespace_span, + }); + name = self.parse_public_identifier()?; + } else { + name = name.replace('_', "-"); + } + + let name = Identifier::from(name); + let name_span = self.toks_mut().span_from(name_start); + + self.whitespace()?; + + let args = if self.toks_mut().next_char_is('(') { + self.parse_argument_invocation(true, false)? + } else { + ArgumentInvocation::empty(self.toks().current_span()) + }; + + self.whitespace()?; + + let content_args = if self.scan_identifier("using", false)? { + self.whitespace()?; + let args = self.parse_argument_declaration()?; + self.whitespace()?; + Some(args) + } else { + None + }; + + let mut content_block: Option = None; + + if content_args.is_some() || self.looking_at_children()? { + let content_args = content_args.unwrap_or_else(ArgumentDeclaration::empty); + let was_in_content_block = self.flags().in_content_block(); + self.flags_mut().set(ContextFlags::IN_CONTENT_BLOCK, true); + let body = self.with_children(Self::parse_statement)?.node; + content_block = Some(AstContentBlock { + args: content_args, + body, + }); + self.flags_mut() + .set(ContextFlags::IN_CONTENT_BLOCK, was_in_content_block); + } else { + self.expect_statement_separator(None)?; + } + + Ok(AstStmt::Include(AstInclude { + namespace, + name: Spanned { + node: name, + span: name_span, + }, + args, + content: content_block, + span: name_span, + })) + } + + fn parse_media_rule(&mut self, start: usize) -> SassResult { + let query = self.parse_media_query_list()?; + + let body = self.with_children(Self::parse_statement)?.node; + + Ok(AstStmt::Media(AstMedia { + query, + body, + span: self.toks_mut().span_from(start), + })) + } + + fn parse_interpolated_string(&mut self) -> SassResult> { + let start = self.toks().cursor(); + let quote = match self.toks_mut().next() { + Some(Token { + kind: kind @ ('"' | '\''), + .. + }) => kind, + Some(..) | None => unreachable!("Expected string."), + }; + + let mut buffer = Interpolation::new(); + + let mut found_match = false; + + while let Some(next) = self.toks().peek() { + match next.kind { + c if c == quote => { + self.toks_mut().next(); + found_match = true; + break; + } + '\n' => break, + '\\' => { + match self.toks().peek_n(1) { + // todo: if (second == $cr) scanner.scanChar($lf); + // we basically need to stop normalizing to gain parity + Some(Token { kind: '\n', .. }) => { + self.toks_mut().next(); + self.toks_mut().next(); + } + _ => buffer.add_char(self.consume_escaped_char()?), + } + } + '#' => { + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { + buffer.add_interpolation(self.parse_single_interpolation()?); + } else { + self.toks_mut().next(); + buffer.add_token(next); + } + } + _ => { + buffer.add_token(next); + self.toks_mut().next(); + } + } + } + + if !found_match { + return Err((format!("Expected {quote}."), self.toks().current_span()).into()); + } + + Ok(Spanned { + node: StringExpr(buffer, QuoteKind::Quoted), + span: self.toks_mut().span_from(start), + }) + } + + fn parse_return_rule(&mut self) -> SassResult { + let value = self.parse_expression(None, None, None)?; + self.expect_statement_separator(None)?; + Ok(AstStmt::Return(AstReturn { + val: value.node, + span: value.span, + })) + } + + fn parse_mixin_rule(&mut self, start: usize) -> SassResult { + let name = Identifier::from(self.parse_identifier(true, false)?); + self.whitespace()?; + let args = if self.toks_mut().next_char_is('(') { + self.parse_argument_declaration()? + } else { + ArgumentDeclaration::empty() + }; + + if self.flags().in_mixin() || self.flags().in_content_block() { + return Err(( + "Mixins may not contain mixin declarations.", + self.toks_mut().span_from(start), + ) + .into()); + } else if self.flags().in_control_flow() { + return Err(( + "Mixins may not be declared in control directives.", + self.toks_mut().span_from(start), + ) + .into()); + } + + self.whitespace()?; + + let old_found_content_rule = self.flags().found_content_rule(); + self.flags_mut() + .set(ContextFlags::FOUND_CONTENT_RULE, false); + self.flags_mut().set(ContextFlags::IN_MIXIN, true); + + let body = self.with_children(Self::parse_statement)?.node; + + let has_content = self.flags_mut().found_content_rule(); + + self.flags_mut() + .set(ContextFlags::FOUND_CONTENT_RULE, old_found_content_rule); + self.flags_mut().set(ContextFlags::IN_MIXIN, false); + + Ok(AstStmt::Mixin(AstMixin { + name, + args, + body, + has_content, + })) + } + + fn _parse_moz_document_rule(&mut self, _name: Interpolation) -> SassResult { + todo!("special cased @-moz-document not yet implemented") + } + + fn unknown_at_rule(&mut self, name: Interpolation, start: usize) -> SassResult { + let was_in_unknown_at_rule = self.flags().in_unknown_at_rule(); + self.flags_mut().set(ContextFlags::IN_UNKNOWN_AT_RULE, true); + + let value: Option = + if !self.toks_mut().next_char_is('!') && !self.at_end_of_statement() { + Some(self.almost_any_value(false)?) + } else { + None + }; + + let children = if self.looking_at_children()? { + Some(self.with_children(Self::parse_statement)?.node) + } else { + self.expect_statement_separator(None)?; + None + }; + + self.flags_mut() + .set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule); + + Ok(AstStmt::UnknownAtRule(AstUnknownAtRule { + name, + value, + children, + span: self.toks_mut().span_from(start), + })) + } + + fn try_supports_operation( + &mut self, + interpolation: &Interpolation, + _start: usize, + ) -> SassResult> { + if interpolation.contents.len() != 1 { + return Ok(None); + } + + let expression = match interpolation.contents.first() { + Some(InterpolationPart::Expr(e)) => e, + Some(InterpolationPart::String(..)) => return Ok(None), + None => unreachable!(), + }; + + let before_whitespace = self.toks().cursor(); + self.whitespace()?; + + let mut operation: Option = None; + let mut operator: Option = None; + + while self.looking_at_identifier() { + if let Some(operator) = &operator { + self.expect_identifier(operator, false)?; + } else if self.scan_identifier("and", false)? { + operator = Some("and".to_owned()); + } else if self.scan_identifier("or", false)? { + operator = Some("or".to_owned()); + } else { + self.toks_mut().set_cursor(before_whitespace); + return Ok(None); + } + + self.whitespace()?; + + let right = self.supports_condition_in_parens()?; + operation = Some(AstSupportsCondition::Operation { + left: Box::new( + operation + .unwrap_or(AstSupportsCondition::Interpolation(expression.clone().node)), + ), + operator: operator.clone(), + right: Box::new(right), + }); + self.whitespace()?; + } + + Ok(operation) + } + + fn supports_declaration_value( + &mut self, + name: AstExpr, + start: usize, + ) -> SassResult { + let value = match &name { + AstExpr::String(StringExpr(text, QuoteKind::None), ..) + if text.initial_plain().starts_with("--") => + { + let text = self.parse_interpolated_declaration_value(false, false, true)?; + AstExpr::String( + StringExpr(text, QuoteKind::None), + self.toks_mut().span_from(start), + ) + } + _ => { + self.whitespace()?; + self.parse_expression(None, None, None)?.node + } + }; + + Ok(AstSupportsCondition::Declaration { name, value }) + } + + fn supports_condition_in_parens(&mut self) -> SassResult { + let start = self.toks().cursor(); + + if self.looking_at_interpolated_identifier() { + let identifier = self.parse_interpolated_identifier()?; + let ident_span = self.toks_mut().span_from(start); + + if identifier.as_plain().unwrap_or("").to_ascii_lowercase() == "not" { + return Err((r#""not" is not a valid identifier here."#, ident_span).into()); + } + + if self.scan_char('(') { + let arguments = self.parse_interpolated_declaration_value(true, true, true)?; + self.expect_char(')')?; + return Ok(AstSupportsCondition::Function { + name: identifier, + args: arguments, + }); + } else if identifier.contents.len() != 1 + || !matches!( + identifier.contents.first(), + Some(InterpolationPart::Expr(..)) + ) + { + return Err(("Expected @supports condition.", ident_span).into()); + } else { + match identifier.contents.first() { + Some(InterpolationPart::Expr(e)) => { + return Ok(AstSupportsCondition::Interpolation(e.clone().node)) + } + _ => unreachable!(), + } + } + } + + self.expect_char('(')?; + self.whitespace()?; + + if self.scan_identifier("not", false)? { + self.whitespace()?; + let condition = self.supports_condition_in_parens()?; + self.expect_char(')')?; + return Ok(AstSupportsCondition::Negation(Box::new(condition))); + } else if self.toks_mut().next_char_is('(') { + let condition = self.parse_supports_condition()?; + self.expect_char(')')?; + return Ok(condition); + } + + // Unfortunately, we may have to backtrack here. The grammar is: + // + // Expression ":" Expression + // | InterpolatedIdentifier InterpolatedAnyValue? + // + // These aren't ambiguous because this `InterpolatedAnyValue` is forbidden + // from containing a top-level colon, but we still have to parse the full + // expression to figure out if there's a colon after it. + // + // We could avoid the overhead of a full expression parse by looking ahead + // for a colon (outside of balanced brackets), but in practice we expect the + // vast majority of real uses to be `Expression ":" Expression`, so it makes + // sense to parse that case faster in exchange for less code complexity and + // a slower backtracking case. + + let name: AstExpr; + let name_start = self.toks().cursor(); + let was_in_parens = self.flags().in_parens(); + + let expr = self.parse_expression(None, None, None); + let found_colon = self.expect_char(':'); + match (expr, found_colon) { + (Ok(val), Ok(..)) => { + name = val.node; + } + (Ok(..), Err(e)) | (Err(e), Ok(..)) | (Err(e), Err(..)) => { + self.toks_mut().set_cursor(name_start); + self.flags_mut().set(ContextFlags::IN_PARENS, was_in_parens); + + let identifier = self.parse_interpolated_identifier()?; + + // todo: superfluous clone? + if let Some(operation) = self.try_supports_operation(&identifier, name_start)? { + self.expect_char(')')?; + return Ok(operation); + } + + // If parsing an expression fails, try to parse an + // `InterpolatedAnyValue` instead. But if that value runs into a + // top-level colon, then this is probably intended to be a declaration + // after all, so we rethrow the declaration-parsing error. + let mut contents = Interpolation::new(); + contents.add_interpolation(identifier); + contents.add_interpolation( + self.parse_interpolated_declaration_value(true, true, false)?, + ); + + if self.toks_mut().next_char_is(':') { + return Err(e); + } + + self.expect_char(')')?; + + return Ok(AstSupportsCondition::Anything { contents }); + } + } + + let declaration = self.supports_declaration_value(name, start)?; + self.expect_char(')')?; + + Ok(declaration) + } + + fn parse_supports_condition(&mut self) -> SassResult { + if self.scan_identifier("not", false)? { + self.whitespace()?; + return Ok(AstSupportsCondition::Negation(Box::new( + self.supports_condition_in_parens()?, + ))); + } + + let mut condition = self.supports_condition_in_parens()?; + self.whitespace()?; + + let mut operator: Option = None; + + while self.looking_at_identifier() { + if let Some(operator) = &operator { + self.expect_identifier(operator, false)?; + } else if self.scan_identifier("or", false)? { + operator = Some("or".to_owned()); + } else { + self.expect_identifier("and", false)?; + operator = Some("and".to_owned()); + } + + self.whitespace()?; + let right = self.supports_condition_in_parens()?; + condition = AstSupportsCondition::Operation { + left: Box::new(condition), + operator: operator.clone(), + right: Box::new(right), + }; + self.whitespace()?; + } + + Ok(condition) + } + + fn parse_supports_rule(&mut self) -> SassResult { + let condition = self.parse_supports_condition()?; + self.whitespace()?; + let children = self.with_children(Self::parse_statement)?; + + Ok(AstStmt::Supports(AstSupportsRule { + condition, + children: children.node, + span: children.span, + })) + } + + fn parse_warn_rule(&mut self) -> SassResult { + let value = self.parse_expression(None, None, None)?; + self.expect_statement_separator(Some("@warn rule"))?; + Ok(AstStmt::Warn(AstWarn { + value: value.node, + span: value.span, + })) + } + + fn parse_while_rule( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult { + let was_in_control_directive = self.flags().in_control_flow(); + self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); + + let condition = self.parse_expression(None, None, None)?.node; + + let body = self.with_children(child)?.node; + + self.flags_mut() + .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); + + Ok(AstStmt::While(AstWhile { condition, body })) + } + fn parse_forward_rule(&mut self, start: usize) -> SassResult { + let url = PathBuf::from(self.parse_url_string()?); + self.whitespace()?; + + let prefix = if self.scan_identifier("as", false)? { + self.whitespace()?; + let prefix = self.parse_identifier(true, false)?; + self.expect_char('*')?; + self.whitespace()?; + Some(prefix) + } else { + None + }; + + let mut shown_mixins_and_functions: Option> = None; + let mut shown_variables: Option> = None; + let mut hidden_mixins_and_functions: Option> = None; + let mut hidden_variables: Option> = None; + + if self.scan_identifier("show", false)? { + let members = self.parse_member_list()?; + shown_mixins_and_functions = Some(members.0); + shown_variables = Some(members.1); + } else if self.scan_identifier("hide", false)? { + let members = self.parse_member_list()?; + hidden_mixins_and_functions = Some(members.0); + hidden_variables = Some(members.1); + } + + let config = self.parse_configuration(true)?; + + self.expect_statement_separator(Some("@forward rule"))?; + let span = self.toks_mut().span_from(start); + + if !self.flags().is_use_allowed() { + return Err(( + "@forward rules must be written before any other rules.", + span, + ) + .into()); + } + + Ok(AstStmt::Forward( + if let (Some(shown_mixins_and_functions), Some(shown_variables)) = + (shown_mixins_and_functions, shown_variables) + { + AstForwardRule::show( + url, + shown_mixins_and_functions, + shown_variables, + prefix, + config, + span, + ) + } else if let (Some(hidden_mixins_and_functions), Some(hidden_variables)) = + (hidden_mixins_and_functions, hidden_variables) + { + AstForwardRule::hide( + url, + hidden_mixins_and_functions, + hidden_variables, + prefix, + config, + span, + ) + } else { + AstForwardRule::new(url, prefix, config, span) + }, + )) + } + + fn parse_member_list(&mut self) -> SassResult<(HashSet, HashSet)> { + let mut identifiers = HashSet::new(); + let mut variables = HashSet::new(); + + loop { + self.whitespace()?; + + // todo: withErrorMessage("Expected variable, mixin, or function name" + if self.toks_mut().next_char_is('$') { + variables.insert(Identifier::from(self.parse_variable_name()?)); + } else { + identifiers.insert(Identifier::from(self.parse_identifier(true, false)?)); + } + + self.whitespace()?; + + if !self.scan_char(',') { + break; + } + } + + Ok((identifiers, variables)) + } + + fn parse_url_string(&mut self) -> SassResult { + // todo: real uri parsing + self.parse_string() + } + + fn use_namespace(&mut self, url: &Path, _start: usize) -> SassResult> { + if self.scan_identifier("as", false)? { + self.whitespace()?; + return Ok(if self.scan_char('*') { + None + } else { + Some(self.parse_identifier(false, false)?) + }); + } + + let base_name = url + .file_name() + .map_or_else(OsString::new, ToOwned::to_owned); + let base_name = base_name.to_string_lossy(); + let dot = base_name.find('.'); + + let start = if base_name.starts_with('_') { 1 } else { 0 }; + let end = dot.unwrap_or(base_name.len()); + let namespace = if url.to_string_lossy().starts_with("sass:") { + return Ok(Some(url.to_string_lossy().into_owned())); + } else { + &base_name[start..end] + }; + + let mut toks = Lexer::new( + namespace + .chars() + .map(|x| Token::new(self.span_before(), x)) + .collect(), + ); + + // if namespace is empty, avoid attempting to parse an identifier from + // an empty string, as there will be no span to emit + let identifier = if namespace.is_empty() { + Err(("", self.span_before()).into()) + } else { + mem::swap(self.toks_mut(), &mut toks); + let ident = self.parse_identifier(false, false); + mem::swap(self.toks_mut(), &mut toks); + ident + }; + + match (identifier, toks.peek().is_none()) { + (Ok(i), true) => Ok(Some(i)), + _ => { + Err(( + format!("The default namespace \"{namespace}\" is not a valid Sass identifier.\n\nRecommendation: add an \"as\" clause to define an explicit namespace."), + self.toks_mut().span_from(start) + ).into()) + } + } + } + + fn parse_configuration( + &mut self, + // default=false + allow_guarded: bool, + ) -> SassResult>> { + if !self.scan_identifier("with", false)? { + return Ok(None); + } + + let mut variable_names = HashSet::new(); + let mut configuration = Vec::new(); + self.whitespace()?; + self.expect_char('(')?; + + loop { + self.whitespace()?; + let var_start = self.toks().cursor(); + let name = Identifier::from(self.parse_variable_name()?); + let name_span = self.toks_mut().span_from(var_start); + self.whitespace()?; + self.expect_char(':')?; + self.whitespace()?; + let expr = self.parse_expression_until_comma(false)?; + + let mut is_guarded = false; + let flag_start = self.toks().cursor(); + if allow_guarded && self.scan_char('!') { + let flag = self.parse_identifier(false, false)?; + if flag == "default" { + is_guarded = true; + self.whitespace()?; + } else { + return Err( + ("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(), + ); + } + } + + let span = self.toks_mut().span_from(var_start); + if variable_names.contains(&name) { + return Err(("The same variable may only be configured once.", span).into()); + } + + variable_names.insert(name); + configuration.push(ConfiguredVariable { + name: Spanned { + node: name, + span: name_span, + }, + expr, + is_guarded, + }); + + if !self.scan_char(',') { + break; + } + self.whitespace()?; + if !self.looking_at_expression() { + break; + } + } + + self.expect_char(')')?; + + Ok(Some(configuration)) + } + + fn parse_use_rule(&mut self, start: usize) -> SassResult { + let url = self.parse_url_string()?; + self.whitespace()?; + + let path = PathBuf::from(url); + + let namespace = self.use_namespace(path.as_ref(), start)?; + self.whitespace()?; + let configuration = self.parse_configuration(false)?; + + self.expect_statement_separator(Some("@use rule"))?; + + let span = self.toks_mut().span_from(start); + + if !self.flags().is_use_allowed() { + return Err(( + "@use rules must be written before any other rules.", + self.toks_mut().span_from(start), + ) + .into()); + } + + self.expect_statement_separator(Some("@use rule"))?; + + Ok(AstStmt::Use(AstUseRule { + url: path, + namespace, + configuration: configuration.unwrap_or_default(), + span, + })) + } + + fn parse_at_rule( + &mut self, + child: fn(&mut Self) -> SassResult, + ) -> SassResult { + let start = self.toks().cursor(); + + self.expect_char('@')?; + let name = self.parse_interpolated_identifier()?; + self.whitespace()?; + + // We want to set [_isUseAllowed] to `false` *unless* we're parsing + // `@charset`, `@forward`, or `@use`. To avoid double-comparing the rule + // name, we always set it to `false` and then set it back to its previous + // value if we're parsing an allowed rule. + let was_use_allowed = self.flags().is_use_allowed(); + self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); + + match name.as_plain() { + Some("at-root") => self.parse_at_root_rule(start), + Some("content") => self.parse_content_rule(start), + Some("debug") => self.parse_debug_rule(), + Some("each") => self.parse_each_rule(child), + Some("else") | Some("return") => self.parse_disallowed_at_rule(start), + Some("error") => self.parse_error_rule(), + Some("extend") => self.parse_extend_rule(start), + Some("for") => self.parse_for_rule(child), + Some("forward") => { + self.flags_mut() + .set(ContextFlags::IS_USE_ALLOWED, was_use_allowed); + // if (!root) { + // _disallowedAtRule(); + // } + self.parse_forward_rule(start) + } + Some("function") => self.parse_function_rule(start), + Some("if") => self.parse_if_rule(child), + Some("import") => self.parse_import_rule(start), + Some("include") => self.parse_include_rule(), + Some("media") => self.parse_media_rule(start), + Some("mixin") => self.parse_mixin_rule(start), + // todo: support -moz-document + // Some("-moz-document") => self.parse_moz_document_rule(name), + Some("supports") => self.parse_supports_rule(), + Some("use") => { + self.flags_mut() + .set(ContextFlags::IS_USE_ALLOWED, was_use_allowed); + // if (!root) { + // _disallowedAtRule(); + // } + self.parse_use_rule(start) + } + Some("warn") => self.parse_warn_rule(), + Some("while") => self.parse_while_rule(child), + Some(..) | None => self.unknown_at_rule(name, start), + } + } + + fn parse_statement(&mut self) -> SassResult { + match self.toks().peek() { + Some(Token { kind: '@', .. }) => self.parse_at_rule(Self::parse_statement), + Some(Token { kind: '+', .. }) => { + if !self.is_indented() { + return self.parse_style_rule(None, None); + } + + let start = self.toks().cursor(); + + self.toks_mut().next(); + + if !self.looking_at_identifier() { + self.toks_mut().set_cursor(start); + return self.parse_style_rule(None, None); + } + + self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); + self.parse_include_rule() + } + Some(Token { kind: '=', .. }) => { + if !self.is_indented() { + return self.parse_style_rule(None, None); + } + + self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); + let start = self.toks().cursor(); + self.toks_mut().next(); + self.whitespace()?; + self.parse_mixin_rule(start) + } + Some(Token { kind: '}', .. }) => { + Err(("unmatched \"}\".", self.toks().current_span()).into()) + } + _ => { + if self.flags().in_style_rule() + || self.flags().in_unknown_at_rule() + || self.flags().in_mixin() + || self.flags().in_content_block() + { + self.parse_declaration_or_style_rule() + } else { + self.parse_variable_declaration_or_style_rule() + } + } + } + } + + fn parse_declaration_or_style_rule(&mut self) -> SassResult { + let start = self.toks().cursor(); + + if self.is_plain_css() && self.flags().in_style_rule() && !self.flags().in_unknown_at_rule() + { + return self.parse_property_or_variable_declaration(true); + } + + // The indented syntax allows a single backslash to distinguish a style rule + // from old-style property syntax. We don't support old property syntax, but + // we do support the backslash because it's easy to do. + if self.is_indented() && self.scan_char('\\') { + return self.parse_style_rule(None, None); + }; + + match self.parse_declaration_or_buffer()? { + DeclarationOrBuffer::Stmt(s) => Ok(s), + DeclarationOrBuffer::Buffer(existing_buffer) => { + self.parse_style_rule(Some(existing_buffer), Some(start)) + } + } + } + + fn parse_property_or_variable_declaration( + &mut self, + // default=true + parse_custom_properties: bool, + ) -> SassResult { + let start = self.toks().cursor(); + + let name = if matches!( + self.toks().peek(), + Some(Token { + kind: ':' | '*' | '.', + .. + }) + ) || (matches!(self.toks().peek(), Some(Token { kind: '#', .. })) + && !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))) + { + // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" + // hacks. + let mut name_buffer = Interpolation::new(); + name_buffer.add_token(self.toks_mut().next().unwrap()); + name_buffer.add_string(self.raw_text(Self::whitespace)); + name_buffer.add_interpolation(self.parse_interpolated_identifier()?); + name_buffer + } else if !self.is_plain_css() { + match self.parse_variable_declaration_or_interpolation()? { + VariableDeclOrInterpolation::Interpolation(interpolation) => interpolation, + VariableDeclOrInterpolation::VariableDecl(decl) => { + return Ok(AstStmt::VariableDecl(decl)) + } + } + } else { + self.parse_interpolated_identifier()? + }; + + self.whitespace()?; + self.expect_char(':')?; + + if parse_custom_properties && name.initial_plain().starts_with("--") { + let interpolation = self.parse_interpolated_declaration_value(false, false, true)?; + let value_span = self.toks_mut().span_from(start); + let value = AstExpr::String(StringExpr(interpolation, QuoteKind::None), value_span) + .span(value_span); + self.expect_statement_separator(Some("custom property"))?; + return Ok(AstStmt::Style(AstStyle { + name, + value: Some(value), + body: Vec::new(), + span: value_span, + })); + } + + self.whitespace()?; + + if self.looking_at_children()? { + if self.is_plain_css() { + return Err(( + "Nested declarations aren't allowed in plain CSS.", + self.toks().current_span(), + ) + .into()); + } + + if name.initial_plain().starts_with("--") { + return Err(( + "Declarations whose names begin with \"--\" may not be nested", + self.toks_mut().span_from(start), + ) + .into()); + } + + let children = self.with_children(Self::parse_declaration_child)?.node; + + return Ok(AstStmt::Style(AstStyle { + name, + value: None, + body: children, + span: self.toks_mut().span_from(start), + })); + } + + let value = self.parse_expression(None, None, None)?; + if self.looking_at_children()? { + if self.is_plain_css() { + return Err(( + "Nested declarations aren't allowed in plain CSS.", + self.toks().current_span(), + ) + .into()); + } + + if name.initial_plain().starts_with("--") && !matches!(value.node, AstExpr::String(..)) + { + return Err(( + "Declarations whose names begin with \"--\" may not be nested", + self.toks_mut().span_from(start), + ) + .into()); + } + + let children = self.with_children(Self::parse_declaration_child)?.node; + + Ok(AstStmt::Style(AstStyle { + name, + value: Some(value), + body: children, + span: self.toks_mut().span_from(start), + })) + } else { + self.expect_statement_separator(None)?; + Ok(AstStmt::Style(AstStyle { + name, + value: Some(value), + body: Vec::new(), + span: self.toks_mut().span_from(start), + })) + } + } + + fn parse_single_interpolation(&mut self) -> SassResult { + self.expect_char('#')?; + self.expect_char('{')?; + self.whitespace()?; + let contents = self.parse_expression(None, None, None)?; + self.expect_char('}')?; + + if self.is_plain_css() { + return Err(("Interpolation isn't allowed in plain CSS.", contents.span).into()); + } + + let mut interpolation = Interpolation::new(); + interpolation + .contents + .push(InterpolationPart::Expr(contents)); + + Ok(interpolation) + } + + fn parse_interpolated_identifier_body(&mut self, buffer: &mut Interpolation) -> SassResult<()> { + while let Some(next) = self.toks().peek() { + match next.kind { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => { + buffer.add_token(next); + self.toks_mut().next(); + } + '\\' => { + buffer.add_string(self.parse_escape(false)?); + } + '#' if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { + buffer.add_interpolation(self.parse_single_interpolation()?); + } + _ => break, + } + } + + Ok(()) + } + + fn parse_interpolated_identifier(&mut self) -> SassResult { + let mut buffer = Interpolation::new(); + + if self.scan_char('-') { + buffer.add_char('-'); + + if self.scan_char('-') { + buffer.add_char('-'); + self.parse_interpolated_identifier_body(&mut buffer)?; + return Ok(buffer); + } + } + + match self.toks().peek() { + Some(tok) if is_name_start(tok.kind) => { + buffer.add_token(tok); + self.toks_mut().next(); + } + Some(Token { kind: '\\', .. }) => { + buffer.add_string(self.parse_escape(true)?); + } + Some(Token { kind: '#', .. }) + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => + { + buffer.add_interpolation(self.parse_single_interpolation()?); + } + Some(..) | None => { + return Err(("Expected identifier.", self.toks().current_span()).into()) + } + } + + self.parse_interpolated_identifier_body(&mut buffer)?; + + Ok(buffer) + } + + fn looking_at_interpolated_identifier(&mut self) -> bool { + let first = match self.toks().peek() { + Some(Token { kind: '\\', .. }) => return true, + Some(Token { kind: '#', .. }) => { + return matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) + } + Some(Token { kind, .. }) if is_name_start(kind) => return true, + Some(tok) => tok, + None => return false, + }; + + if first.kind != '-' { + return false; + } + + match self.toks().peek_n(1) { + Some(Token { kind: '#', .. }) => { + matches!(self.toks().peek_n(2), Some(Token { kind: '{', .. })) + } + Some(Token { + kind: '\\' | '-', .. + }) => true, + Some(Token { kind, .. }) => is_name_start(kind), + None => false, + } + } + + fn parse_loud_comment(&mut self) -> SassResult { + let start = self.toks().cursor(); + self.expect_char('/')?; + self.expect_char('*')?; + + let mut buffer = Interpolation::new_plain("/*".to_owned()); + + while let Some(tok) = self.toks().peek() { + match tok.kind { + '#' => { + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { + buffer.add_interpolation(self.parse_single_interpolation()?); + } else { + self.toks_mut().next(); + buffer.add_token(tok); + } + } + '*' => { + self.toks_mut().next(); + buffer.add_token(tok); + + if self.scan_char('/') { + buffer.add_char('/'); + + return Ok(AstLoudComment { + text: buffer, + span: self.toks_mut().span_from(start), + }); + } + } + '\r' => { + self.toks_mut().next(); + // todo: does \r even exist at this point? (removed by lexer) + if !self.toks_mut().next_char_is('\n') { + buffer.add_char('\n'); + } + } + _ => { + buffer.add_token(tok); + self.toks_mut().next(); + } + } + } + + Err(("expected more input.", self.toks().current_span()).into()) + } + + fn parse_interpolated_declaration_value( + &mut self, + // default=false + allow_semicolon: bool, + // default=false + allow_empty: bool, + // default=true + allow_colon: bool, + ) -> SassResult { + let mut buffer = Interpolation::new(); + + let mut brackets = Vec::new(); + let mut wrote_newline = false; + + while let Some(tok) = self.toks().peek() { + match tok.kind { + '\\' => { + buffer.add_string(self.parse_escape(true)?); + wrote_newline = false; + } + '"' | '\'' => { + buffer.add_interpolation( + self.parse_interpolated_string()? + .node + .as_interpolation(false), + ); + wrote_newline = false; + } + '/' => { + if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) { + let comment = self.fallible_raw_text(Self::skip_loud_comment)?; + buffer.add_string(comment); + } else { + self.toks_mut().next(); + buffer.add_token(tok); + } + + wrote_newline = false; + } + '#' => { + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { + // Add a full interpolated identifier to handle cases like + // "#{...}--1", since "--1" isn't a valid identifier on its own. + buffer.add_interpolation(self.parse_interpolated_identifier()?); + } else { + self.toks_mut().next(); + buffer.add_token(tok); + } + + wrote_newline = false; + } + ' ' | '\t' => { + if wrote_newline + || !matches!( + self.toks().peek_n(1), + Some(Token { + kind: ' ' | '\r' | '\t' | '\n', + .. + }) + ) + { + self.toks_mut().next(); + buffer.add_token(tok); + } else { + self.toks_mut().next(); + } + } + '\n' | '\r' => { + if self.is_indented() { + break; + } + if !matches!( + self.toks().peek_n_backwards(1), + Some(Token { + kind: '\r' | '\n', + .. + }) + ) { + buffer.add_char('\n'); + } + self.toks_mut().next(); + wrote_newline = true; + } + '(' | '{' | '[' => { + self.toks_mut().next(); + buffer.add_token(tok); + brackets.push(opposite_bracket(tok.kind)); + wrote_newline = false; + } + ')' | '}' | ']' => { + if brackets.is_empty() { + break; + } + buffer.add_token(tok); + self.expect_char(brackets.pop().unwrap())?; + wrote_newline = false; + } + ';' => { + if !allow_semicolon && brackets.is_empty() { + break; + } + buffer.add_token(tok); + self.toks_mut().next(); + wrote_newline = false; + } + ':' => { + if !allow_colon && brackets.is_empty() { + break; + } + buffer.add_token(tok); + self.toks_mut().next(); + wrote_newline = false; + } + 'u' | 'U' => { + let before_url = self.toks().cursor(); + + if !self.scan_identifier("url", false)? { + buffer.add_token(tok); + self.toks_mut().next(); + wrote_newline = false; + continue; + } + + match self.try_url_contents(None)? { + Some(contents) => { + buffer.add_interpolation(contents); + } + None => { + self.toks_mut().set_cursor(before_url); + buffer.add_token(tok); + self.toks_mut().next(); + } + } + + wrote_newline = false; + } + _ => { + if self.looking_at_identifier() { + buffer.add_string(self.parse_identifier(false, false)?); + } else { + buffer.add_token(tok); + self.toks_mut().next(); + } + wrote_newline = false; + } + } + } + + if let Some(&last) = brackets.last() { + self.expect_char(last)?; + } + + if !allow_empty && buffer.contents.is_empty() { + return Err(("Expected token.", self.toks().current_span()).into()); + } + + Ok(buffer) + } + + fn parse_expression_until_comma( + &mut self, + // default=false + single_equals: bool, + ) -> SassResult> { + ValueParser::parse_expression( + self, + Some(&|parser| { + Ok(matches!( + parser.toks().peek(), + Some(Token { kind: ',', .. }) + )) + }), + false, + single_equals, + ) + } + + fn parse_argument_invocation( + &mut self, + for_mixin: bool, + allow_empty_second_arg: bool, + ) -> SassResult { + let start = self.toks().cursor(); + + self.expect_char('(')?; + self.whitespace()?; + + let mut positional = Vec::new(); + let mut named = BTreeMap::new(); + + let mut rest: Option = None; + let mut keyword_rest: Option = None; + + while self.looking_at_expression() { + let expression = self.parse_expression_until_comma(!for_mixin)?; + self.whitespace()?; + + if expression.node.is_variable() && self.scan_char(':') { + let name = match expression.node { + AstExpr::Variable { name, .. } => name, + _ => unreachable!(), + }; + + self.whitespace()?; + if named.contains_key(&name.node) { + return Err(("Duplicate argument.", name.span).into()); + } + + named.insert( + name.node, + self.parse_expression_until_comma(!for_mixin)?.node, + ); + } else if self.scan_char('.') { + self.expect_char('.')?; + self.expect_char('.')?; + + if rest.is_none() { + rest = Some(expression.node); + } else { + keyword_rest = Some(expression.node); + self.whitespace()?; + break; + } + } else if !named.is_empty() { + return Err(( + "Positional arguments must come before keyword arguments.", + expression.span, + ) + .into()); + } else { + positional.push(expression.node); + } + + self.whitespace()?; + if !self.scan_char(',') { + break; + } + self.whitespace()?; + + if allow_empty_second_arg + && positional.len() == 1 + && named.is_empty() + && rest.is_none() + && matches!(self.toks().peek(), Some(Token { kind: ')', .. })) + { + positional.push(AstExpr::String( + StringExpr(Interpolation::new(), QuoteKind::None), + self.toks().current_span(), + )); + break; + } + } + + self.expect_char(')')?; + + Ok(ArgumentInvocation { + positional, + named, + rest, + keyword_rest, + span: self.toks_mut().span_from(start), + }) + } + + fn parse_expression( + &mut self, + parse_until: Option>, + inside_bracketed_list: Option, + single_equals: Option, + ) -> SassResult> { + ValueParser::parse_expression( + self, + parse_until, + inside_bracketed_list.unwrap_or(false), + single_equals.unwrap_or(false), + ) + } + + fn parse_declaration_or_buffer(&mut self) -> SassResult { + let start = self.toks().cursor(); + let mut name_buffer = Interpolation::new(); + + // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" + // hacks. + let first = self.toks().peek(); + let mut starts_with_punctuation = false; + + if matches!( + first, + Some(Token { + kind: ':' | '*' | '.', + .. + }) + ) || (matches!(first, Some(Token { kind: '#', .. })) + && !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))) + { + starts_with_punctuation = true; + name_buffer.add_token(self.toks_mut().next().unwrap()); + name_buffer.add_string(self.raw_text(Self::whitespace)); + } + + if !self.looking_at_interpolated_identifier() { + return Ok(DeclarationOrBuffer::Buffer(name_buffer)); + } + + let variable_or_interpolation = if starts_with_punctuation { + VariableDeclOrInterpolation::Interpolation(self.parse_interpolated_identifier()?) + } else { + self.parse_variable_declaration_or_interpolation()? + }; + + match variable_or_interpolation { + VariableDeclOrInterpolation::Interpolation(int) => name_buffer.add_interpolation(int), + VariableDeclOrInterpolation::VariableDecl(v) => { + return Ok(DeclarationOrBuffer::Stmt(AstStmt::VariableDecl(v))) + } + } + + self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); + + if self.next_matches("/*") { + name_buffer.add_string(self.fallible_raw_text(Self::skip_loud_comment)?); + } + + let mut mid_buffer = String::new(); + mid_buffer.push_str(&self.raw_text(Self::whitespace)); + + if !self.scan_char(':') { + if !mid_buffer.is_empty() { + name_buffer.add_char(' '); + } + return Ok(DeclarationOrBuffer::Buffer(name_buffer)); + } + mid_buffer.push(':'); + + // Parse custom properties as declarations no matter what. + if name_buffer.initial_plain().starts_with("--") { + let value_start = self.toks().cursor(); + let value = self.parse_interpolated_declaration_value(false, false, true)?; + let value_span = self.toks_mut().span_from(value_start); + self.expect_statement_separator(Some("custom property"))?; + return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { + name: name_buffer, + value: Some( + AstExpr::String(StringExpr(value, QuoteKind::None), value_span) + .span(value_span), + ), + span: self.toks_mut().span_from(start), + body: Vec::new(), + }))); + } + + if self.scan_char(':') { + name_buffer.add_string(mid_buffer); + name_buffer.add_char(':'); + return Ok(DeclarationOrBuffer::Buffer(name_buffer)); + } else if self.is_indented() && self.looking_at_interpolated_identifier() { + // In the indented syntax, `foo:bar` is always considered a selector + // rather than a property. + name_buffer.add_string(mid_buffer); + return Ok(DeclarationOrBuffer::Buffer(name_buffer)); + } + + let post_colon_whitespace = self.raw_text(Self::whitespace); + if self.looking_at_children()? { + let body = self.with_children(Self::parse_declaration_child)?.node; + return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { + name: name_buffer, + value: None, + span: self.toks_mut().span_from(start), + body, + }))); + } + + mid_buffer.push_str(&post_colon_whitespace); + let could_be_selector = + post_colon_whitespace.is_empty() && self.looking_at_interpolated_identifier(); + + let before_decl = self.toks().cursor(); + + let mut calculate_value = || { + let value = self.parse_expression(None, None, None)?; + + if self.looking_at_children()? { + if could_be_selector { + self.expect_statement_separator(None)?; + } + } else if !self.at_end_of_statement() { + self.expect_statement_separator(None)?; + } + + Ok(value) + }; + + let value = match calculate_value() { + Ok(v) => v, + Err(e) => { + if !could_be_selector { + return Err(e); + } + + self.toks_mut().set_cursor(before_decl); + let additional = self.almost_any_value(false)?; + if !self.is_indented() && self.toks_mut().next_char_is(';') { + return Err(e); + } + + name_buffer.add_string(mid_buffer); + name_buffer.add_interpolation(additional); + return Ok(DeclarationOrBuffer::Buffer(name_buffer)); + } + }; + + if self.looking_at_children()? { + let body = self.with_children(Self::parse_declaration_child)?.node; + Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { + name: name_buffer, + value: Some(value), + span: self.toks_mut().span_from(start), + body, + }))) + } else { + self.expect_statement_separator(None)?; + Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { + name: name_buffer, + value: Some(value), + span: self.toks_mut().span_from(start), + body: Vec::new(), + }))) + } + } + + fn parse_declaration_child(&mut self) -> SassResult { + let start = self.toks().cursor(); + + if self.toks_mut().next_char_is('@') { + self.parse_declaration_at_rule(start) + } else { + self.parse_property_or_variable_declaration(false) + } + } + + fn parse_plain_at_rule_name(&mut self) -> SassResult { + self.expect_char('@')?; + let name = self.parse_identifier(false, false)?; + self.whitespace()?; + Ok(name) + } + + fn parse_declaration_at_rule(&mut self, start: usize) -> SassResult { + let name = self.parse_plain_at_rule_name()?; + + match name.as_str() { + "content" => self.parse_content_rule(start), + "debug" => self.parse_debug_rule(), + "each" => self.parse_each_rule(Self::parse_declaration_child), + "else" => self.parse_disallowed_at_rule(start), + "error" => self.parse_error_rule(), + "for" => self.parse_for_rule(Self::parse_declaration_child), + "if" => self.parse_if_rule(Self::parse_declaration_child), + "include" => self.parse_include_rule(), + "warn" => self.parse_warn_rule(), + "while" => self.parse_while_rule(Self::parse_declaration_child), + _ => self.parse_disallowed_at_rule(start), + } + } + + fn parse_variable_declaration_or_style_rule(&mut self) -> SassResult { + let start = self.toks().cursor(); + + if self.is_plain_css() { + return self.parse_style_rule(None, None); + } + + // The indented syntax allows a single backslash to distinguish a style rule + // from old-style property syntax. We don't support old property syntax, but + // we do support the backslash because it's easy to do. + if self.is_indented() && self.scan_char('\\') { + return self.parse_style_rule(None, None); + }; + + if !self.looking_at_identifier() { + return self.parse_style_rule(None, None); + } + + match self.parse_variable_declaration_or_interpolation()? { + VariableDeclOrInterpolation::VariableDecl(var) => Ok(AstStmt::VariableDecl(var)), + VariableDeclOrInterpolation::Interpolation(int) => { + self.parse_style_rule(Some(int), Some(start)) + } + } + } + + fn parse_style_rule( + &mut self, + existing_buffer: Option, + start: Option, + ) -> SassResult { + let start = start.unwrap_or_else(|| self.toks().cursor()); + + self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); + let mut interpolation = self.parse_style_rule_selector()?; + + if let Some(mut existing_buffer) = existing_buffer { + existing_buffer.add_interpolation(interpolation); + interpolation = existing_buffer; + } + + if interpolation.contents.is_empty() { + return Err(("expected \"}\".", self.toks().current_span()).into()); + } + + let was_in_style_rule = self.flags().in_style_rule(); + *self.flags_mut() |= ContextFlags::IN_STYLE_RULE; + + let selector_span = self.toks_mut().span_from(start); + + let children = self.with_children(Self::parse_statement)?; + + self.flags_mut() + .set(ContextFlags::IN_STYLE_RULE, was_in_style_rule); + + let span = selector_span.merge(children.span); + + Ok(AstStmt::RuleSet(AstRuleSet { + selector: interpolation, + body: children.node, + selector_span, + span, + })) + } + + fn parse_silent_comment(&mut self) -> SassResult { + let start = self.toks().cursor(); + debug_assert!(self.next_matches("//")); + self.toks_mut().next(); + self.toks_mut().next(); + + let mut buffer = String::new(); + + while let Some(tok) = self.toks_mut().next() { + if tok.kind == '\n' { + self.whitespace_without_comments(); + if self.next_matches("//") { + self.toks_mut().next(); + self.toks_mut().next(); + buffer.clear(); + continue; + } + break; + } + + buffer.push(tok.kind); + } + + if self.is_plain_css() { + return Err(( + "Silent comments aren't allowed in plain CSS.", + self.toks_mut().span_from(start), + ) + .into()); + } + + self.whitespace_without_comments(); + + Ok(AstStmt::SilentComment(AstSilentComment { + text: buffer, + span: self.toks_mut().span_from(start), + })) + } + + fn next_is_hex(&self) -> bool { + match self.toks().peek() { + Some(Token { kind, .. }) => kind.is_ascii_hexdigit(), + None => false, + } + } + + fn assert_public(ident: &str, span: Span) -> SassResult<()> { + if !ScssParser::is_private(ident) { + return Ok(()); + } + + Err(( + "Private members can't be accessed from outside their modules.", + span, + ) + .into()) + } + + fn is_private(ident: &str) -> bool { + ident.starts_with('-') || ident.starts_with('_') + } + + fn parse_variable_declaration_without_namespace( + &mut self, + namespace: Option>, + start: Option, + ) -> SassResult { + let start = start.unwrap_or_else(|| self.toks().cursor()); + + let name = self.parse_variable_name()?; + + if namespace.is_some() { + Self::assert_public(&name, self.toks_mut().span_from(start))?; + } + + if self.is_plain_css() { + return Err(( + "Sass variables aren't allowed in plain CSS.", + self.toks_mut().span_from(start), + ) + .into()); + } + + self.whitespace()?; + self.expect_char(':')?; + self.whitespace()?; + + let value = self.parse_expression(None, None, None)?.node; + + let mut is_guarded = false; + let mut is_global = false; + + while self.scan_char('!') { + let flag_start = self.toks().cursor(); + let flag = self.parse_identifier(false, false)?; + + match flag.as_str() { + "default" => is_guarded = true, + "global" => { + if namespace.is_some() { + return Err(( + "!global isn't allowed for variables in other modules.", + self.toks_mut().span_from(flag_start), + ) + .into()); + } + + is_global = true; + } + _ => { + return Err( + ("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(), + ) + } + } + + self.whitespace()?; + } + + self.expect_statement_separator(Some("variable declaration"))?; + + let declaration = AstVariableDecl { + namespace, + name: Identifier::from(name), + value, + is_guarded, + is_global, + span: self.toks_mut().span_from(start), + }; + + if is_global { + // todo + // _globalVariables.putIfAbsent(name, () => declaration) + } + + Ok(declaration) + } + + fn almost_any_value( + &mut self, + // default=false + omit_comments: bool, + ) -> SassResult { + let mut buffer = Interpolation::new(); + + while let Some(tok) = self.toks().peek() { + match tok.kind { + '\\' => { + // Write a literal backslash because this text will be re-parsed. + buffer.add_token(tok); + self.toks_mut().next(); + match self.toks_mut().next() { + Some(tok) => buffer.add_token(tok), + None => { + return Err(("expected more input.", self.toks().current_span()).into()) + } + } + } + '"' | '\'' => { + buffer.add_interpolation( + self.parse_interpolated_string()? + .node + .as_interpolation(false), + ); + } + '/' => { + let comment_start = self.toks().cursor(); + if self.scan_comment()? { + if !omit_comments { + buffer.add_string(self.toks().raw_text(comment_start)); + } + } else { + buffer.add_token(self.toks_mut().next().unwrap()); + } + } + '#' => { + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { + // Add a full interpolated identifier to handle cases like + // "#{...}--1", since "--1" isn't a valid identifier on its own. + buffer.add_interpolation(self.parse_interpolated_identifier()?); + } else { + self.toks_mut().next(); + buffer.add_token(tok); + } + } + '\r' | '\n' => { + if self.is_indented() { + break; + } + buffer.add_token(self.toks_mut().next().unwrap()); + } + '!' | ';' | '{' | '}' => break, + 'u' | 'U' => { + let before_url = self.toks().cursor(); + if !self.scan_identifier("url", false)? { + self.toks_mut().next(); + buffer.add_token(tok); + continue; + } + + match self.try_url_contents(None)? { + Some(contents) => buffer.add_interpolation(contents), + None => { + self.toks_mut().set_cursor(before_url); + self.toks_mut().next(); + buffer.add_token(tok); + } + } + } + _ => { + if self.looking_at_identifier() { + buffer.add_string(self.parse_identifier(false, false)?); + } else { + buffer.add_token(self.toks_mut().next().unwrap()); + } + } + } + } + + Ok(buffer) + } + + fn parse_variable_declaration_or_interpolation( + &mut self, + ) -> SassResult { + if !self.looking_at_identifier() { + return Ok(VariableDeclOrInterpolation::Interpolation( + self.parse_interpolated_identifier()?, + )); + } + + let start = self.toks().cursor(); + + let ident = self.parse_identifier(false, false)?; + if self.next_matches(".$") { + let namespace_span = self.toks_mut().span_from(start); + self.expect_char('.')?; + Ok(VariableDeclOrInterpolation::VariableDecl( + self.parse_variable_declaration_without_namespace( + Some(Spanned { + node: Identifier::from(ident), + span: namespace_span, + }), + Some(start), + )?, + )) + } else { + let mut buffer = Interpolation::new_plain(ident); + + if self.looking_at_interpolated_identifier_body() { + buffer.add_interpolation(self.parse_interpolated_identifier()?); + } + + Ok(VariableDeclOrInterpolation::Interpolation(buffer)) + } + } + + fn looking_at_interpolated_identifier_body(&mut self) -> bool { + match self.toks().peek() { + Some(Token { kind: '\\', .. }) => true, + Some(Token { kind: '#', .. }) + if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => + { + true + } + Some(Token { kind, .. }) if is_name(kind) => true, + Some(..) | None => false, + } + } + + fn expression_until_comparison(&mut self) -> SassResult> { + let value = self.parse_expression( + Some(&|parser| { + Ok(match parser.toks().peek() { + Some(Token { kind: '>', .. }) | Some(Token { kind: '<', .. }) => true, + Some(Token { kind: '=', .. }) => { + !matches!(parser.toks().peek_n(1), Some(Token { kind: '=', .. })) + } + _ => false, + }) + }), + None, + None, + )?; + Ok(value) + } + + fn parse_media_query_list(&mut self) -> SassResult { + let mut buf = Interpolation::new(); + loop { + self.whitespace()?; + self.parse_media_query(&mut buf)?; + self.whitespace()?; + if !self.scan_char(',') { + break; + } + buf.add_char(','); + buf.add_char(' '); + } + Ok(buf) + } + + fn parse_media_in_parens(&mut self, buf: &mut Interpolation) -> SassResult<()> { + self.expect_char_with_message('(', "media condition in parentheses")?; + buf.add_char('('); + self.whitespace()?; + + if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) { + self.parse_media_in_parens(buf)?; + self.whitespace()?; + + if self.scan_identifier("and", false)? { + buf.add_string(" and ".to_owned()); + self.expect_whitespace()?; + self.parse_media_logic_sequence(buf, "and")?; + } else if self.scan_identifier("or", false)? { + buf.add_string(" or ".to_owned()); + self.expect_whitespace()?; + self.parse_media_logic_sequence(buf, "or")?; + } + } else if self.scan_identifier("not", false)? { + buf.add_string("not ".to_owned()); + self.expect_whitespace()?; + self.parse_media_or_interpolation(buf)?; + } else { + buf.add_expr(self.expression_until_comparison()?); + + if self.scan_char(':') { + self.whitespace()?; + buf.add_char(':'); + buf.add_char(' '); + buf.add_expr(self.parse_expression(None, None, None)?); + } else { + let next = self.toks().peek(); + if matches!( + next, + Some(Token { + kind: '<' | '>' | '=', + .. + }) + ) { + let next = next.unwrap().kind; + buf.add_char(' '); + buf.add_token(self.toks_mut().next().unwrap()); + + if (next == '<' || next == '>') && self.scan_char('=') { + buf.add_char('='); + } + + buf.add_char(' '); + + self.whitespace()?; + + buf.add_expr(self.expression_until_comparison()?); + + if (next == '<' || next == '>') && self.scan_char(next) { + buf.add_char(' '); + buf.add_char(next); + + if self.scan_char('=') { + buf.add_char('='); + } + + buf.add_char(' '); + + self.whitespace()?; + buf.add_expr(self.expression_until_comparison()?); + } + } + } + } + + self.expect_char(')')?; + self.whitespace()?; + buf.add_char(')'); + + Ok(()) + } + + fn parse_media_logic_sequence( + &mut self, + buf: &mut Interpolation, + operator: &'static str, + ) -> SassResult<()> { + loop { + self.parse_media_or_interpolation(buf)?; + self.whitespace()?; + + if !self.scan_identifier(operator, false)? { + return Ok(()); + } + + self.expect_whitespace()?; + + buf.add_char(' '); + buf.add_string(operator.to_owned()); + buf.add_char(' '); + } + } + + fn parse_media_or_interpolation(&mut self, buf: &mut Interpolation) -> SassResult<()> { + if self.toks_mut().next_char_is('#') { + buf.add_interpolation(self.parse_single_interpolation()?); + } else { + self.parse_media_in_parens(buf)?; + } + + Ok(()) + } + + fn parse_media_query(&mut self, buf: &mut Interpolation) -> SassResult<()> { + if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) { + self.parse_media_in_parens(buf)?; + self.whitespace()?; + + if self.scan_identifier("and", false)? { + buf.add_string(" and ".to_owned()); + self.expect_whitespace()?; + self.parse_media_logic_sequence(buf, "and")?; + } else if self.scan_identifier("or", false)? { + buf.add_string(" or ".to_owned()); + self.expect_whitespace()?; + self.parse_media_logic_sequence(buf, "or")?; + } + + return Ok(()); + } + + let ident1 = self.parse_interpolated_identifier()?; + + if ident1.as_plain().unwrap_or("").to_ascii_lowercase() == "not" { + // For example, "@media not (...) {" + self.expect_whitespace()?; + if !self.looking_at_interpolated_identifier() { + buf.add_string("not ".to_owned()); + self.parse_media_or_interpolation(buf)?; + return Ok(()); + } + } + + self.whitespace()?; + buf.add_interpolation(ident1); + if !self.looking_at_interpolated_identifier() { + // For example, "@media screen {". + return Ok(()); + } + + buf.add_char(' '); + + let ident2 = self.parse_interpolated_identifier()?; + + if ident2.as_plain().unwrap_or("").to_ascii_lowercase() == "and" { + self.expect_whitespace()?; + // For example, "@media screen and ..." + buf.add_string(" and ".to_owned()); + } else { + self.whitespace()?; + buf.add_interpolation(ident2); + + if self.scan_identifier("and", false)? { + // For example, "@media only screen and ..." + self.expect_whitespace()?; + buf.add_string(" and ".to_owned()); + } else { + // For example, "@media only screen {" + return Ok(()); + } + } + + // 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()?; + buf.add_string("not ".to_owned()); + self.parse_media_or_interpolation(buf)?; + return Ok(()); + } + + self.parse_media_logic_sequence(buf, "and")?; + + Ok(()) + } +} diff --git a/src/parse/throw_away.rs b/src/parse/throw_away.rs deleted file mode 100644 index 9952aaab..00000000 --- a/src/parse/throw_away.rs +++ /dev/null @@ -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()) - } -} diff --git a/src/parse/value.rs b/src/parse/value.rs new file mode 100644 index 00000000..f688045b --- /dev/null +++ b/src/parse/value.rs @@ -0,0 +1,1826 @@ +use std::{iter::Iterator, marker::PhantomData}; + +use codemap::Spanned; + +use crate::{ + ast::*, + color::{Color, ColorFormat, NAMED_COLORS}, + common::{unvendor, BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind, UnaryOp}, + error::SassResult, + unit::Unit, + utils::{as_hex, opposite_bracket}, + value::{CalculationName, Number}, + ContextFlags, Token, +}; + +use super::StylesheetParser; + +pub(crate) type Predicate<'c, P> = &'c dyn Fn(&mut P) -> SassResult; + +fn is_hex_color(interpolation: &Interpolation) -> bool { + if let Some(plain) = interpolation.as_plain() { + if ![3, 4, 6, 8].contains(&plain.len()) { + return false; + } + + return plain.chars().all(|c| c.is_ascii_hexdigit()); + } + + false +} + +pub(crate) struct ValueParser<'a, 'c, P: StylesheetParser<'a>> { + comma_expressions: Option>>, + space_expressions: Option>>, + binary_operators: Option>, + operands: Option>>, + allow_slash: bool, + single_expression: Option>, + start: usize, + inside_bracketed_list: bool, + single_equals: bool, + parse_until: Option>, + _a: PhantomData<&'a ()>, +} + +impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { + pub fn parse_expression( + parser: &mut P, + parse_until: Option>, + inside_bracketed_list: bool, + single_equals: bool, + ) -> SassResult> { + let start = parser.toks().cursor(); + let mut value_parser = Self::new(parser, parse_until, inside_bracketed_list, single_equals); + + if let Some(parse_until) = value_parser.parse_until { + if parse_until(parser)? { + return Err(("Expected expression.", parser.toks().current_span()).into()); + } + } + + if value_parser.inside_bracketed_list { + let start = parser.toks().cursor(); + + parser.expect_char('[')?; + parser.whitespace()?; + + if parser.scan_char(']') { + return Ok(AstExpr::List(ListExpr { + elems: Vec::new(), + separator: ListSeparator::Undecided, + brackets: Brackets::Bracketed, + }) + .span(parser.toks_mut().span_from(start))); + } + + Some(start) + } else { + None + }; + + value_parser.single_expression = Some(value_parser.parse_single_expression(parser)?); + + let mut value = value_parser.parse_value(parser)?; + value.span = parser.toks_mut().span_from(start); + + Ok(value) + } + + pub fn new( + parser: &mut P, + parse_until: Option>, + inside_bracketed_list: bool, + single_equals: bool, + ) -> Self { + Self { + comma_expressions: None, + space_expressions: None, + binary_operators: None, + operands: None, + allow_slash: true, + start: parser.toks().cursor(), + single_expression: None, + parse_until, + inside_bracketed_list, + single_equals, + _a: PhantomData, + } + } + + /// Parse a value from a stream of tokens + /// + /// This function will cease parsing if the predicate returns true. + pub(crate) fn parse_value(&mut self, parser: &mut P) -> SassResult> { + parser.whitespace()?; + + let start = parser.toks().cursor(); + + let was_in_parens = parser.flags().in_parens(); + + loop { + parser.whitespace()?; + + if let Some(parse_until) = self.parse_until { + if parse_until(parser)? { + break; + } + } + + let first = parser.toks().peek(); + + match first { + Some(Token { kind: '(', .. }) => { + let expr = self.parse_paren_expr(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '[', .. }) => { + let expr = parser.parse_expression(None, Some(true), None)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '$', .. }) => { + let expr = Self::parse_variable(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '&', .. }) => { + let expr = Self::parse_selector(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '"', .. }) | Some(Token { kind: '\'', .. }) => { + let expr = parser + .parse_interpolated_string()? + .map_node(|s| AstExpr::String(s, parser.toks_mut().span_from(start))); + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '#', .. }) => { + let expr = self.parse_hash(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '=', .. }) => { + parser.toks_mut().next(); + if self.single_equals + && !matches!(parser.toks().peek(), Some(Token { kind: '=', .. })) + { + self.add_operator( + Spanned { + node: BinaryOp::SingleEq, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } else { + parser.expect_char('=')?; + self.add_operator( + Spanned { + node: BinaryOp::Equal, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + } + Some(Token { kind: '!', .. }) => match parser.toks().peek_n(1) { + Some(Token { kind: '=', .. }) => { + parser.toks_mut().next(); + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: BinaryOp::NotEqual, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + Some(Token { kind, .. }) + if kind.is_ascii_whitespace() || kind == 'i' || kind == 'I' => + { + let expr = Self::parse_important_expr(parser)?; + self.add_single_expression(expr, parser)?; + } + None => { + let expr = Self::parse_important_expr(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(..) => break, + }, + Some(Token { kind: '<', .. }) => { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: if parser.scan_char('=') { + BinaryOp::LessThanEqual + } else { + BinaryOp::LessThan + }, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + Some(Token { kind: '>', .. }) => { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: if parser.scan_char('=') { + BinaryOp::GreaterThanEqual + } else { + BinaryOp::GreaterThan + }, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + Some(Token { kind: '*', pos }) => { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: BinaryOp::Mul, + span: pos, + }, + parser, + )?; + } + Some(Token { kind: '+', .. }) => { + if self.single_expression.is_none() { + let expr = self.parse_unary_operation(parser)?; + self.add_single_expression(expr, parser)?; + } else { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: BinaryOp::Plus, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + } + Some(Token { kind: '-', .. }) => { + if matches!( + parser.toks().peek_n(1), + Some(Token { + kind: '0'..='9' | '.', + .. + }) + ) && (self.single_expression.is_none() + || matches!( + parser.toks_mut().peek_previous(), + Some(Token { + kind: ' ' | '\t' | '\n' | '\r', + .. + }) + )) + { + let expr = ValueParser::parse_number(parser)?; + self.add_single_expression(expr, parser)?; + } else if parser.looking_at_interpolated_identifier() { + let expr = self.parse_identifier_like(parser)?; + self.add_single_expression(expr, parser)?; + } else if self.single_expression.is_none() { + let expr = self.parse_unary_operation(parser)?; + self.add_single_expression(expr, parser)?; + } else { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: BinaryOp::Minus, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + } + Some(Token { kind: '/', .. }) => { + if self.single_expression.is_none() { + let expr = self.parse_unary_operation(parser)?; + self.add_single_expression(expr, parser)?; + } else { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: BinaryOp::Div, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } + } + Some(Token { kind: '%', pos }) => { + parser.toks_mut().next(); + self.add_operator( + Spanned { + node: BinaryOp::Rem, + span: pos, + }, + parser, + )?; + } + Some(Token { + kind: '0'..='9', .. + }) => { + let expr = ValueParser::parse_number(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: '.', .. }) => { + if matches!(parser.toks().peek_n(1), Some(Token { kind: '.', .. })) { + break; + } + let expr = ValueParser::parse_number(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: 'a', .. }) => { + if !parser.is_plain_css() && parser.scan_identifier("and", false)? { + self.add_operator( + Spanned { + node: BinaryOp::And, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } else { + let expr = self.parse_identifier_like(parser)?; + self.add_single_expression(expr, parser)?; + } + } + Some(Token { kind: 'o', .. }) => { + if !parser.is_plain_css() && parser.scan_identifier("or", false)? { + self.add_operator( + Spanned { + node: BinaryOp::Or, + span: parser.toks_mut().span_from(start), + }, + parser, + )?; + } else { + let expr = self.parse_identifier_like(parser)?; + self.add_single_expression(expr, parser)?; + } + } + Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) => { + if matches!(parser.toks().peek_n(1), Some(Token { kind: '+', .. })) { + let expr = Self::parse_unicode_range(parser)?; + self.add_single_expression(expr, parser)?; + } else { + let expr = self.parse_identifier_like(parser)?; + self.add_single_expression(expr, parser)?; + } + } + Some(Token { + kind: 'b'..='z', .. + }) + | Some(Token { + kind: 'A'..='Z', .. + }) + | Some(Token { kind: '_', .. }) + | Some(Token { kind: '\\', .. }) + | Some(Token { + kind: '\u{80}'..=std::char::MAX, + .. + }) => { + let expr = self.parse_identifier_like(parser)?; + self.add_single_expression(expr, parser)?; + } + Some(Token { kind: ',', .. }) => { + // If we discover we're parsing a list whose first element is a + // division operation, and we're in parentheses, reparse outside of a + // paren context. This ensures that `(1/2, 1)` doesn't perform division + // on its first element. + if parser.flags().in_parens() { + parser.flags_mut().set(ContextFlags::IN_PARENS, false); + if self.allow_slash { + self.reset_state(parser)?; + continue; + } + } + + if self.single_expression.is_none() { + return Err(("Expected expression.", parser.toks().current_span()).into()); + } + + self.resolve_space_expressions(parser)?; + + // [resolveSpaceExpressions can modify [singleExpression_], but it + // can't set it to null`. + self.comma_expressions + .get_or_insert_with(Default::default) + .push(self.single_expression.take().unwrap()); + parser.toks_mut().next(); + self.allow_slash = true; + } + Some(..) | None => break, + } + } + + if self.inside_bracketed_list { + parser.expect_char(']')?; + } + + if self.comma_expressions.is_some() { + self.resolve_space_expressions(parser)?; + + parser + .flags_mut() + .set(ContextFlags::IN_PARENS, was_in_parens); + + if let Some(single_expression) = self.single_expression.take() { + self.comma_expressions + .as_mut() + .unwrap() + .push(single_expression); + } + + Ok(AstExpr::List(ListExpr { + elems: self.comma_expressions.take().unwrap(), + separator: ListSeparator::Comma, + brackets: if self.inside_bracketed_list { + Brackets::Bracketed + } else { + Brackets::None + }, + }) + .span(parser.toks_mut().span_from(start))) + } else if self.inside_bracketed_list && self.space_expressions.is_some() { + self.resolve_operations(parser)?; + + self.space_expressions + .as_mut() + .unwrap() + .push(self.single_expression.take().unwrap()); + + Ok(AstExpr::List(ListExpr { + elems: self.space_expressions.take().unwrap(), + separator: ListSeparator::Space, + brackets: Brackets::Bracketed, + }) + .span(parser.toks_mut().span_from(start))) + } else { + self.resolve_space_expressions(parser)?; + + if self.inside_bracketed_list { + return Ok(AstExpr::List(ListExpr { + elems: vec![self.single_expression.take().unwrap()], + separator: ListSeparator::Undecided, + brackets: Brackets::Bracketed, + }) + .span(parser.toks_mut().span_from(start))); + } + + Ok(self.single_expression.take().unwrap()) + } + } + + fn parse_single_expression(&mut self, parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + let first = parser.toks().peek(); + + match first { + Some(Token { kind: '(', .. }) => self.parse_paren_expr(parser), + Some(Token { kind: '/', .. }) => self.parse_unary_operation(parser), + Some(Token { kind: '[', .. }) => Self::parse_expression(parser, None, true, false), + Some(Token { kind: '$', .. }) => Self::parse_variable(parser), + Some(Token { kind: '&', .. }) => Self::parse_selector(parser), + Some(Token { kind: '"', .. }) | Some(Token { kind: '\'', .. }) => Ok(parser + .parse_interpolated_string()? + .map_node(|s| AstExpr::String(s, parser.toks_mut().span_from(start)))), + Some(Token { kind: '#', .. }) => self.parse_hash(parser), + Some(Token { kind: '+', .. }) => self.parse_plus_expr(parser), + Some(Token { kind: '-', .. }) => self.parse_minus_expr(parser), + Some(Token { kind: '!', .. }) => Self::parse_important_expr(parser), + Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) => { + if matches!(parser.toks().peek_n(1), Some(Token { kind: '+', .. })) { + Self::parse_unicode_range(parser) + } else { + self.parse_identifier_like(parser) + } + } + Some(Token { + kind: '0'..='9', .. + }) + | Some(Token { kind: '.', .. }) => ValueParser::parse_number(parser), + Some(Token { + kind: 'a'..='z', .. + }) + | Some(Token { + kind: 'A'..='Z', .. + }) + | Some(Token { kind: '_', .. }) + | Some(Token { kind: '\\', .. }) + | Some(Token { + kind: '\u{80}'..=std::char::MAX, + .. + }) => self.parse_identifier_like(parser), + Some(..) | None => Err(("Expected expression.", parser.toks().current_span()).into()), + } + } + + fn resolve_one_operation(&mut self, parser: &mut P) -> SassResult<()> { + let operator = self.binary_operators.as_mut().unwrap().pop().unwrap(); + let operands = self.operands.as_mut().unwrap(); + + let left = operands.pop().unwrap(); + let right = match self.single_expression.take() { + Some(val) => val, + None => return Err(("Expected expression.", left.span).into()), + }; + + let span = left.span.merge(right.span); + + if self.allow_slash + && !parser.flags().in_parens() + && operator == BinaryOp::Div + && left.node.is_slash_operand() + && right.node.is_slash_operand() + { + self.single_expression = Some(AstExpr::slash(left.node, right.node, span).span(span)); + } else { + self.single_expression = Some( + AstExpr::BinaryOp { + lhs: Box::new(left.node), + op: operator, + rhs: Box::new(right.node), + allows_slash: false, + span, + } + .span(span), + ); + self.allow_slash = false; + } + + Ok(()) + } + + fn resolve_operations(&mut self, parser: &mut P) -> SassResult<()> { + loop { + let should_break = match self.binary_operators.as_ref() { + Some(bin) => bin.is_empty(), + None => true, + }; + + if should_break { + break; + } + + self.resolve_one_operation(parser)?; + } + + Ok(()) + } + + fn add_single_expression( + &mut self, + expression: Spanned, + parser: &mut P, + ) -> SassResult<()> { + if self.single_expression.is_some() { + // If we discover we're parsing a list whose first element is a division + // operation, and we're in parentheses, reparse outside of a paren + // context. This ensures that `(1/2 1)` doesn't perform division on its + // first element. + if parser.flags().in_parens() { + parser.flags_mut().set(ContextFlags::IN_PARENS, false); + + if self.allow_slash { + self.reset_state(parser)?; + + return Ok(()); + } + } + + if self.space_expressions.is_none() { + self.space_expressions = Some(Vec::new()); + } + + self.resolve_operations(parser)?; + + self.space_expressions + .as_mut() + .unwrap() + .push(self.single_expression.take().unwrap()); + + self.allow_slash = true; + } + + self.single_expression = Some(expression); + + Ok(()) + } + + fn add_operator(&mut self, op: Spanned, parser: &mut P) -> SassResult<()> { + if parser.is_plain_css() && op.node != BinaryOp::Div && op.node != BinaryOp::SingleEq { + return Err(("Operators aren't allowed in plain CSS.", op.span).into()); + } + + self.allow_slash = self.allow_slash && op.node == BinaryOp::Div; + + if self.binary_operators.is_none() { + self.binary_operators = Some(Vec::new()); + } + + if self.operands.is_none() { + self.operands = Some(Vec::new()); + } + + while let Some(last_op) = self.binary_operators.as_ref().unwrap_or(&Vec::new()).last() { + if last_op.precedence() < op.precedence() { + break; + } + + self.resolve_one_operation(parser)?; + } + self.binary_operators + .get_or_insert_with(Default::default) + .push(op.node); + + match self.single_expression.take() { + Some(expr) => { + self.operands.get_or_insert_with(Vec::new).push(expr); + } + None => return Err(("Expected expression.", op.span).into()), + } + + parser.whitespace()?; + + self.single_expression = Some(self.parse_single_expression(parser)?); + + Ok(()) + } + + fn resolve_space_expressions(&mut self, parser: &mut P) -> SassResult<()> { + self.resolve_operations(parser)?; + + if let Some(mut space_expressions) = self.space_expressions.take() { + let single_expression = match self.single_expression.take() { + Some(val) => val, + None => return Err(("Expected expression.", parser.toks().current_span()).into()), + }; + + let span = single_expression.span; + + space_expressions.push(single_expression); + + self.single_expression = Some( + AstExpr::List(ListExpr { + elems: space_expressions, + separator: ListSeparator::Space, + brackets: Brackets::None, + }) + .span(span), + ); + } + + Ok(()) + } + + fn parse_map( + parser: &mut P, + first: Spanned, + start: usize, + ) -> SassResult> { + let mut pairs = vec![(first, parser.parse_expression_until_comma(false)?.node)]; + + while parser.scan_char(',') { + parser.whitespace()?; + if !parser.looking_at_expression() { + break; + } + + let key = parser.parse_expression_until_comma(false)?; + parser.expect_char(':')?; + parser.whitespace()?; + let value = parser.parse_expression_until_comma(false)?; + pairs.push((key, value.node)); + } + + parser.expect_char(')')?; + + Ok(AstExpr::Map(AstSassMap(pairs)).span(parser.toks_mut().span_from(start))) + } + + fn parse_paren_expr(&mut self, parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + if parser.is_plain_css() { + return Err(( + "Parentheses aren't allowed in plain CSS.", + parser.toks().current_span(), + ) + .into()); + } + + let was_in_parentheses = parser.flags().in_parens(); + parser.flags_mut().set(ContextFlags::IN_PARENS, true); + + parser.expect_char('(')?; + parser.whitespace()?; + if !parser.looking_at_expression() { + parser.expect_char(')')?; + parser + .flags_mut() + .set(ContextFlags::IN_PARENS, was_in_parentheses); + return Ok(AstExpr::List(ListExpr { + elems: Vec::new(), + separator: ListSeparator::Undecided, + brackets: Brackets::None, + }) + .span(parser.toks_mut().span_from(start))); + } + + let first = parser.parse_expression_until_comma(false)?; + if parser.scan_char(':') { + parser.whitespace()?; + parser + .flags_mut() + .set(ContextFlags::IN_PARENS, was_in_parentheses); + return Self::parse_map(parser, first, start); + } + + if !parser.scan_char(',') { + parser.expect_char(')')?; + parser + .flags_mut() + .set(ContextFlags::IN_PARENS, was_in_parentheses); + return Ok(AstExpr::Paren(Box::new(first.node)).span(first.span)); + } + + parser.whitespace()?; + + let mut expressions = vec![first]; + + loop { + if !parser.looking_at_expression() { + break; + } + expressions.push(parser.parse_expression_until_comma(false)?); + if !parser.scan_char(',') { + break; + } + parser.whitespace()?; + } + + parser.expect_char(')')?; + + parser + .flags_mut() + .set(ContextFlags::IN_PARENS, was_in_parentheses); + + Ok(AstExpr::List(ListExpr { + elems: expressions, + separator: ListSeparator::Comma, + brackets: Brackets::None, + }) + .span(parser.toks_mut().span_from(start))) + } + + fn parse_variable(parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + let name = parser.parse_variable_name()?; + + if parser.is_plain_css() { + return Err(( + "Sass variables aren't allowed in plain CSS.", + parser.toks_mut().span_from(start), + ) + .into()); + } + + Ok(AstExpr::Variable { + name: Spanned { + node: Identifier::from(name), + span: parser.toks_mut().span_from(start), + }, + namespace: None, + } + .span(parser.toks_mut().span_from(start))) + } + + fn parse_selector(parser: &mut P) -> SassResult> { + if parser.is_plain_css() { + return Err(( + "The parent selector isn't allowed in plain CSS.", + parser.toks().current_span(), + ) + .into()); + } + + let start = parser.toks().cursor(); + + parser.expect_char('&')?; + + if parser.toks().next_char_is('&') { + // todo: emit a warning here + // warn( + // 'In Sass, "&&" means two copies of the parent selector. You ' + // 'probably want to use "and" instead.', + // scanner.spanFrom(start)); + // scanner.position--; + } + + Ok(AstExpr::ParentSelector.span(parser.toks_mut().span_from(start))) + } + + fn parse_hash(&mut self, parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + debug_assert!(matches!( + parser.toks().peek(), + Some(Token { kind: '#', .. }) + )); + + if matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { + return self.parse_identifier_like(parser); + } + + parser.expect_char('#')?; + + if matches!( + parser.toks().peek(), + Some(Token { + kind: '0'..='9', + .. + }) + ) { + let color = self.parse_hex_color_contents(parser)?; + return Ok(AstExpr::Color(Box::new(color)).span(parser.toks_mut().span_from(start))); + } + + let after_hash = parser.toks().cursor(); + let ident = parser.parse_interpolated_identifier()?; + if is_hex_color(&ident) { + parser.toks_mut().set_cursor(after_hash); + let color = self.parse_hex_color_contents(parser)?; + return Ok( + AstExpr::Color(Box::new(color)).span(parser.toks_mut().span_from(after_hash)) + ); + } + + let mut buffer = Interpolation::new(); + + buffer.add_char('#'); + buffer.add_interpolation(ident); + + let span = parser.toks_mut().span_from(start); + + Ok(AstExpr::String(StringExpr(buffer, QuoteKind::None), span).span(span)) + } + + fn parse_hex_digit(&mut self, parser: &mut P) -> SassResult { + match parser.toks().peek() { + Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => { + parser.toks_mut().next(); + Ok(as_hex(kind)) + } + _ => Err(("Expected hex digit.", parser.toks().current_span()).into()), + } + } + + fn parse_hex_color_contents(&mut self, parser: &mut P) -> SassResult { + let start = parser.toks().cursor(); + + let digit1 = self.parse_hex_digit(parser)?; + let digit2 = self.parse_hex_digit(parser)?; + let digit3 = self.parse_hex_digit(parser)?; + + let red: u32; + let green: u32; + let blue: u32; + let mut alpha: f64 = 1.0; + + if parser.next_is_hex() { + let digit4 = self.parse_hex_digit(parser)?; + + if parser.next_is_hex() { + red = (digit1 << 4) + digit2; + green = (digit3 << 4) + digit4; + blue = (self.parse_hex_digit(parser)? << 4) + self.parse_hex_digit(parser)?; + + if parser.next_is_hex() { + alpha = ((self.parse_hex_digit(parser)? << 4) + self.parse_hex_digit(parser)?) + as f64 + / 0xff as f64; + } + } else { + // #abcd + red = (digit1 << 4) + digit1; + green = (digit2 << 4) + digit2; + blue = (digit3 << 4) + digit3; + alpha = ((digit4 << 4) + digit4) as f64 / 0xff as f64; + } + } else { + // #abc + red = (digit1 << 4) + digit1; + green = (digit2 << 4) + digit2; + blue = (digit3 << 4) + digit3; + } + + Ok(Color::new_rgba( + Number::from(red), + Number::from(green), + Number::from(blue), + Number::from(alpha), + // todo: + // // Don't emit four- or eight-digit hex colors as hex, since that's not + // // yet well-supported in browsers. + ColorFormat::Literal(parser.toks_mut().raw_text(start - 1)), + )) + } + + fn parse_unary_operation(&mut self, parser: &mut P) -> SassResult> { + let op_span = parser.toks().current_span(); + let operator = Self::expect_unary_operator(parser)?; + + if parser.is_plain_css() && operator != UnaryOp::Div { + return Err(("Operators aren't allowed in plain CSS.", op_span).into()); + } + + parser.whitespace()?; + + let operand = self.parse_single_expression(parser)?; + + let span = op_span.merge(parser.toks().current_span()); + + Ok(AstExpr::UnaryOp(operator, Box::new(operand.node), span).span(span)) + } + + fn expect_unary_operator(parser: &mut P) -> SassResult { + let span = parser.toks().current_span(); + Ok(match parser.toks_mut().next() { + Some(Token { kind: '+', .. }) => UnaryOp::Plus, + Some(Token { kind: '-', .. }) => UnaryOp::Neg, + Some(Token { kind: '/', .. }) => UnaryOp::Div, + Some(..) | None => return Err(("Expected unary operator.", span).into()), + }) + } + + fn consume_natural_number(parser: &mut P) -> SassResult<()> { + if !matches!( + parser.toks_mut().next(), + Some(Token { + kind: '0'..='9', + .. + }) + ) { + return Err(("Expected digit.", parser.toks().prev_span()).into()); + } + + while matches!( + parser.toks().peek(), + Some(Token { + kind: '0'..='9', + .. + }) + ) { + parser.toks_mut().next(); + } + + Ok(()) + } + + fn parse_number(parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + + if !parser.scan_char('+') { + parser.scan_char('-'); + } + + let after_sign = parser.toks().cursor(); + + if !parser.toks().next_char_is('.') { + ValueParser::consume_natural_number(parser)?; + } + + ValueParser::try_decimal(parser, parser.toks().cursor() != after_sign)?; + ValueParser::try_exponent(parser)?; + + let number: f64 = parser.toks_mut().raw_text(start).parse().unwrap(); + + let unit = if parser.scan_char('%') { + Unit::Percent + } else if parser.looking_at_identifier() + && (!matches!(parser.toks().peek(), Some(Token { kind: '-', .. })) + || !matches!(parser.toks().peek_n(1), Some(Token { kind: '-', .. }))) + { + Unit::from(parser.parse_identifier(false, true)?) + } else { + Unit::None + }; + + Ok(AstExpr::Number { + n: Number::from(number), + unit, + } + .span(parser.toks_mut().span_from(start))) + } + + fn try_decimal(parser: &mut P, allow_trailing_dot: bool) -> SassResult> { + if !matches!(parser.toks().peek(), Some(Token { kind: '.', .. })) { + return Ok(None); + } + + match parser.toks().peek_n(1) { + Some(Token { kind, pos }) if !kind.is_ascii_digit() => { + if allow_trailing_dot { + return Ok(None); + } + + return Err(("Expected digit.", pos).into()); + } + Some(..) => {} + None => return Err(("Expected digit.", parser.toks().current_span()).into()), + } + + let mut buffer = String::new(); + + parser.expect_char('.')?; + buffer.push('.'); + + while let Some(Token { kind, .. }) = parser.toks().peek() { + if !kind.is_ascii_digit() { + break; + } + buffer.push(kind); + parser.toks_mut().next(); + } + + Ok(Some(buffer)) + } + + fn try_exponent(parser: &mut P) -> SassResult> { + let mut buffer = String::new(); + + match parser.toks().peek() { + Some(Token { + kind: 'e' | 'E', .. + }) => buffer.push('e'), + _ => return Ok(None), + } + + let next = match parser.toks().peek_n(1) { + Some(Token { + kind: kind @ ('0'..='9' | '-' | '+'), + .. + }) => kind, + _ => return Ok(None), + }; + + parser.toks_mut().next(); + + if next == '+' || next == '-' { + parser.toks_mut().next(); + buffer.push(next); + } + + match parser.toks().peek() { + Some(Token { + kind: '0'..='9', .. + }) => {} + _ => return Err(("Expected digit.", parser.toks().current_span()).into()), + } + + while let Some(tok) = parser.toks().peek() { + if !tok.kind.is_ascii_digit() { + break; + } + + buffer.push(tok.kind); + + parser.toks_mut().next(); + } + + Ok(Some(buffer)) + } + + fn parse_plus_expr(&mut self, parser: &mut P) -> SassResult> { + debug_assert!(parser.toks().next_char_is('+')); + + match parser.toks().peek_n(1) { + Some(Token { + kind: '0'..='9' | '.', + .. + }) => ValueParser::parse_number(parser), + _ => self.parse_unary_operation(parser), + } + } + + fn parse_minus_expr(&mut self, parser: &mut P) -> SassResult> { + debug_assert!(parser.toks().next_char_is('-')); + + if matches!( + parser.toks().peek_n(1), + Some(Token { + kind: '0'..='9' | '.', + .. + }) + ) { + return ValueParser::parse_number(parser); + } + + if parser.looking_at_interpolated_identifier() { + return self.parse_identifier_like(parser); + } + + self.parse_unary_operation(parser) + } + + fn parse_important_expr(parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + parser.expect_char('!')?; + parser.whitespace()?; + parser.expect_identifier("important", false)?; + + let span = parser.toks_mut().span_from(start); + + Ok(AstExpr::String( + StringExpr( + Interpolation::new_plain("!important".to_owned()), + QuoteKind::None, + ), + span, + ) + .span(span)) + } + + fn parse_identifier_like(&mut self, parser: &mut P) -> SassResult> { + if let Some(func) = P::IDENTIFIER_LIKE { + return func(parser); + } + + let start = parser.toks().cursor(); + + let identifier = parser.parse_interpolated_identifier()?; + + let ident_span = parser.toks_mut().span_from(start); + + let plain = identifier.as_plain(); + let lower = plain.map(str::to_ascii_lowercase); + + if let Some(plain) = plain { + if plain == "if" && parser.toks().next_char_is('(') { + let call_args = parser.parse_argument_invocation(false, false)?; + let span = call_args.span; + return Ok(AstExpr::If(Box::new(Ternary(call_args))).span(span)); + } else if plain == "not" { + parser.whitespace()?; + + let value = self.parse_single_expression(parser)?; + + let span = parser.toks_mut().span_from(start); + + return Ok(AstExpr::UnaryOp(UnaryOp::Not, Box::new(value.node), span).span(span)); + } + + let lower_ref = lower.as_ref().unwrap(); + + if !parser.toks().next_char_is('(') { + match plain { + "null" => return Ok(AstExpr::Null.span(parser.toks_mut().span_from(start))), + "true" => return Ok(AstExpr::True.span(parser.toks_mut().span_from(start))), + "false" => return Ok(AstExpr::False.span(parser.toks_mut().span_from(start))), + _ => {} + } + + if let Some(color) = NAMED_COLORS.get_by_name(lower_ref.as_str()) { + return Ok(AstExpr::Color(Box::new(Color::new( + color[0], + color[1], + color[2], + color[3], + plain.to_owned(), + ))) + .span(parser.toks_mut().span_from(start))); + } + } + + if let Some(func) = ValueParser::try_parse_special_function(parser, lower_ref, start)? { + return Ok(func); + } + } + + match parser.toks().peek() { + Some(Token { kind: '.', .. }) => { + if matches!(parser.toks().peek_n(1), Some(Token { kind: '.', .. })) { + return Ok(AstExpr::String( + StringExpr(identifier, QuoteKind::None), + parser.toks_mut().span_from(start), + ) + .span(parser.toks_mut().span_from(start))); + } + parser.toks_mut().next(); + + match plain { + Some(s) => Self::namespaced_expression( + Spanned { + node: Identifier::from(s), + span: ident_span, + }, + start, + parser, + ), + None => Err(("Interpolation isn't allowed in namespaces.", ident_span).into()), + } + } + Some(Token { kind: '(', .. }) => { + if let Some(plain) = plain { + let arguments = + parser.parse_argument_invocation(false, lower.as_deref() == Some("var"))?; + + Ok(AstExpr::FunctionCall(FunctionCallExpr { + namespace: None, + name: Identifier::from(plain), + arguments: Box::new(arguments), + span: parser.toks_mut().span_from(start), + }) + .span(parser.toks_mut().span_from(start))) + } else { + let arguments = parser.parse_argument_invocation(false, false)?; + Ok(AstExpr::InterpolatedFunction(InterpolatedFunction { + name: identifier, + arguments: Box::new(arguments), + span: parser.toks_mut().span_from(start), + }) + .span(parser.toks_mut().span_from(start))) + } + } + _ => Ok(AstExpr::String( + StringExpr(identifier, QuoteKind::None), + parser.toks_mut().span_from(start), + ) + .span(parser.toks_mut().span_from(start))), + } + } + + fn namespaced_expression( + namespace: Spanned, + start: usize, + parser: &mut P, + ) -> SassResult> { + if parser.toks().next_char_is('$') { + let name_start = parser.toks().cursor(); + let name = parser.parse_variable_name()?; + let span = parser.toks_mut().span_from(start); + P::assert_public(&name, span)?; + + if parser.is_plain_css() { + return Err(("Module namespaces aren't allowed in plain CSS.", span).into()); + } + + return Ok(AstExpr::Variable { + name: Spanned { + node: Identifier::from(name), + span: parser.toks_mut().span_from(name_start), + }, + namespace: Some(namespace), + } + .span(span)); + } + + let name = parser.parse_public_identifier()?; + let args = parser.parse_argument_invocation(false, false)?; + let span = parser.toks_mut().span_from(start); + + if parser.is_plain_css() { + return Err(("Module namespaces aren't allowed in plain CSS.", span).into()); + } + + Ok(AstExpr::FunctionCall(FunctionCallExpr { + namespace: Some(namespace), + name: Identifier::from(name), + arguments: Box::new(args), + span, + }) + .span(span)) + } + + fn parse_unicode_range(parser: &mut P) -> SassResult> { + let start = parser.toks().cursor(); + parser.expect_ident_char('u', false)?; + parser.expect_char('+')?; + + let mut first_range_length = 0; + + while let Some(next) = parser.toks().peek() { + if !next.kind.is_ascii_hexdigit() { + break; + } + + parser.toks_mut().next(); + first_range_length += 1; + } + + let mut has_question_mark = false; + + while parser.scan_char('?') { + has_question_mark = true; + first_range_length += 1; + } + + let span = parser.toks_mut().span_from(start); + if first_range_length == 0 { + return Err(("Expected hex digit or \"?\".", parser.toks().current_span()).into()); + } else if first_range_length > 6 { + return Err(("Expected at most 6 digits.", span).into()); + } else if has_question_mark { + return Ok(AstExpr::String( + StringExpr( + Interpolation::new_plain(parser.toks_mut().raw_text(start)), + QuoteKind::None, + ), + span, + ) + .span(span)); + } + + if parser.scan_char('-') { + let second_range_start = parser.toks().cursor(); + let mut second_range_length = 0; + + while let Some(next) = parser.toks().peek() { + if !next.kind.is_ascii_hexdigit() { + break; + } + + parser.toks_mut().next(); + second_range_length += 1; + } + + if second_range_length == 0 { + return Err(("Expected hex digit.", parser.toks().current_span()).into()); + } else if second_range_length > 6 { + return Err(( + "Expected at most 6 digits.", + parser.toks_mut().span_from(second_range_start), + ) + .into()); + } + } + + if parser.looking_at_interpolated_identifier_body() { + return Err(("Expected end of identifier.", parser.toks().current_span()).into()); + } + + let span = parser.toks_mut().span_from(start); + + Ok(AstExpr::String( + StringExpr( + Interpolation::new_plain(parser.toks_mut().raw_text(start)), + QuoteKind::None, + ), + span, + ) + .span(span)) + } + + fn try_parse_url_contents( + parser: &mut P, + name: Option, + ) -> SassResult> { + let start = parser.toks().cursor(); + + if !parser.scan_char('(') { + return Ok(None); + } + + parser.whitespace_without_comments(); + + // 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 = Interpolation::new(); + buffer.add_string(name.unwrap_or_else(|| "url".to_owned())); + buffer.add_char('('); + + while let Some(next) = parser.toks().peek() { + match next.kind { + '\\' => { + buffer.add_string(parser.parse_escape(false)?); + } + '!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { + parser.toks_mut().next(); + buffer.add_token(next); + } + '#' => { + if matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { + buffer.add_interpolation(parser.parse_single_interpolation()?); + } else { + parser.toks_mut().next(); + buffer.add_token(next); + } + } + ')' => { + parser.toks_mut().next(); + buffer.add_token(next); + + return Ok(Some(buffer)); + } + ' ' | '\t' | '\n' | '\r' => { + parser.whitespace_without_comments(); + + if !parser.toks().next_char_is(')') { + break; + } + } + _ => break, + } + } + + parser.toks_mut().set_cursor(start); + Ok(None) + } + + pub(crate) fn try_parse_special_function( + parser: &mut P, + name: &str, + start: usize, + ) -> SassResult>> { + if matches!(parser.toks().peek(), Some(Token { kind: '(', .. })) { + if let Some(calculation) = ValueParser::try_parse_calculation(parser, name, start)? { + return Ok(Some(calculation)); + } + } + + let normalized = unvendor(name); + + let mut buffer; + + match normalized { + "calc" | "element" | "expression" => { + if !parser.scan_char('(') { + return Ok(None); + } + + buffer = Interpolation::new_plain(name.to_owned()); + buffer.add_char('('); + } + "progid" => { + if !parser.scan_char(':') { + return Ok(None); + } + buffer = Interpolation::new_plain(name.to_owned()); + buffer.add_char(':'); + + while let Some(Token { kind, .. }) = parser.toks().peek() { + if !kind.is_alphabetic() && kind != '.' { + break; + } + buffer.add_char(kind); + parser.toks_mut().next(); + } + parser.expect_char('(')?; + buffer.add_char('('); + } + "url" => { + return Ok( + ValueParser::try_parse_url_contents(parser, None)?.map(|contents| { + AstExpr::String( + StringExpr(contents, QuoteKind::None), + parser.toks_mut().span_from(start), + ) + .span(parser.toks_mut().span_from(start)) + }), + ) + } + _ => return Ok(None), + } + + buffer.add_interpolation(parser.parse_interpolated_declaration_value(false, true, true)?); + parser.expect_char(')')?; + buffer.add_char(')'); + + Ok(Some( + AstExpr::String( + StringExpr(buffer, QuoteKind::None), + parser.toks_mut().span_from(start), + ) + .span(parser.toks_mut().span_from(start)), + )) + } + + fn contains_calculation_interpolation(parser: &mut P) -> SassResult { + let mut parens = 0; + let mut brackets = Vec::new(); + + let start = parser.toks().cursor(); + + while let Some(next) = parser.toks().peek() { + match next.kind { + '\\' => { + parser.toks_mut().next(); + // todo: i wonder if this can be broken (not for us but dart-sass) + parser.toks_mut().next(); + } + '/' => { + if !parser.scan_comment()? { + parser.toks_mut().next(); + } + } + '\'' | '"' => { + parser.parse_interpolated_string()?; + } + '#' => { + if parens == 0 + && matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) + { + parser.toks_mut().set_cursor(start); + return Ok(true); + } + parser.toks_mut().next(); + } + '(' | '{' | '[' => { + if next.kind == '(' { + parens += 1; + } + brackets.push(opposite_bracket(next.kind)); + parser.toks_mut().next(); + } + ')' | '}' | ']' => { + if next.kind == ')' { + parens -= 1; + } + if brackets.is_empty() || brackets.pop() != Some(next.kind) { + parser.toks_mut().set_cursor(start); + return Ok(false); + } + parser.toks_mut().next(); + } + _ => { + parser.toks_mut().next(); + } + } + } + + parser.toks_mut().set_cursor(start); + Ok(false) + } + + fn try_parse_calculation_interpolation( + parser: &mut P, + start: usize, + ) -> SassResult> { + Ok( + if ValueParser::contains_calculation_interpolation(parser)? { + Some(AstExpr::String( + StringExpr( + parser.parse_interpolated_declaration_value(false, false, true)?, + QuoteKind::None, + ), + parser.toks_mut().span_from(start), + )) + } else { + None + }, + ) + } + + fn parse_calculation_value(parser: &mut P) -> SassResult> { + match parser.toks().peek() { + Some(Token { + kind: '+' | '-' | '.' | '0'..='9', + .. + }) => ValueParser::parse_number(parser), + Some(Token { kind: '$', .. }) => ValueParser::parse_variable(parser), + Some(Token { kind: '(', .. }) => { + let start = parser.toks().cursor(); + parser.toks_mut().next(); + + let value = match ValueParser::try_parse_calculation_interpolation(parser, start)? { + Some(v) => v, + None => { + parser.whitespace()?; + ValueParser::parse_calculation_sum(parser)?.node + } + }; + + parser.whitespace()?; + parser.expect_char(')')?; + + Ok(AstExpr::Paren(Box::new(value)).span(parser.toks_mut().span_from(start))) + } + _ if !parser.looking_at_identifier() => Err(( + "Expected number, variable, function, or calculation.", + parser.toks().current_span(), + ) + .into()), + _ => { + let start = parser.toks().cursor(); + let ident = parser.parse_identifier(false, false)?; + let ident_span = parser.toks_mut().span_from(start); + if parser.scan_char('.') { + return ValueParser::namespaced_expression( + Spanned { + node: Identifier::from(&ident), + span: ident_span, + }, + start, + parser, + ); + } + + if !parser.toks().next_char_is('(') { + return Err(("Expected \"(\" or \".\".", parser.toks().current_span()).into()); + } + + let lowercase = ident.to_ascii_lowercase(); + let calculation = ValueParser::try_parse_calculation(parser, &lowercase, start)?; + + if let Some(calc) = calculation { + Ok(calc) + } else if lowercase == "if" { + Ok(AstExpr::If(Box::new(Ternary( + parser.parse_argument_invocation(false, false)?, + ))) + .span(parser.toks_mut().span_from(start))) + } else { + Ok(AstExpr::FunctionCall(FunctionCallExpr { + namespace: None, + name: Identifier::from(ident), + arguments: Box::new(parser.parse_argument_invocation(false, false)?), + span: parser.toks_mut().span_from(start), + }) + .span(parser.toks_mut().span_from(start))) + } + } + } + } + fn parse_calculation_product(parser: &mut P) -> SassResult> { + let mut product = ValueParser::parse_calculation_value(parser)?; + + loop { + parser.whitespace()?; + match parser.toks().peek() { + Some(Token { + kind: op @ ('*' | '/'), + .. + }) => { + parser.toks_mut().next(); + parser.whitespace()?; + + let rhs = ValueParser::parse_calculation_value(parser)?; + + let span = product.span.merge(rhs.span); + + product.node = AstExpr::BinaryOp { + lhs: Box::new(product.node), + op: if op == '*' { + BinaryOp::Mul + } else { + BinaryOp::Div + }, + rhs: Box::new(rhs.node), + allows_slash: false, + span, + }; + + product.span = span; + } + _ => return Ok(product), + } + } + } + fn parse_calculation_sum(parser: &mut P) -> SassResult> { + let mut sum = ValueParser::parse_calculation_product(parser)?; + + loop { + match parser.toks().peek() { + Some(Token { + kind: next @ ('+' | '-'), + pos, + }) => { + if !matches!( + parser.toks().peek_n_backwards(1), + Some(Token { + kind: ' ' | '\t' | '\r' | '\n', + .. + }) + ) || !matches!( + parser.toks().peek_n(1), + Some(Token { + kind: ' ' | '\t' | '\r' | '\n', + .. + }) + ) { + return Err(( + "\"+\" and \"-\" must be surrounded by whitespace in calculations.", + pos, + ) + .into()); + } + + parser.toks_mut().next(); + parser.whitespace()?; + + let rhs = ValueParser::parse_calculation_product(parser)?; + + let span = sum.span.merge(rhs.span); + + sum = AstExpr::BinaryOp { + lhs: Box::new(sum.node), + op: if next == '+' { + BinaryOp::Plus + } else { + BinaryOp::Minus + }, + rhs: Box::new(rhs.node), + allows_slash: false, + span, + } + .span(span); + } + _ => return Ok(sum), + } + } + } + + fn parse_calculation_arguments( + parser: &mut P, + max_args: Option, + start: usize, + ) -> SassResult> { + parser.expect_char('(')?; + if let Some(interpolation) = + ValueParser::try_parse_calculation_interpolation(parser, start)? + { + parser.expect_char(')')?; + return Ok(vec![interpolation]); + } + + parser.whitespace()?; + let mut arguments = vec![ValueParser::parse_calculation_sum(parser)?.node]; + + while (max_args.is_none() || arguments.len() < max_args.unwrap()) && parser.scan_char(',') { + parser.whitespace()?; + arguments.push(ValueParser::parse_calculation_sum(parser)?.node); + } + + parser.expect_char_with_message( + ')', + if Some(arguments.len()) == max_args { + r#""+", "-", "*", "/", or ")""# + } else { + r#""+", "-", "*", "/", ",", or ")""# + }, + )?; + + Ok(arguments) + } + + fn try_parse_calculation( + parser: &mut P, + name: &str, + start: usize, + ) -> SassResult>> { + debug_assert!(parser.toks().next_char_is('(')); + + Ok(Some(match name { + "calc" => { + let args = ValueParser::parse_calculation_arguments(parser, Some(1), start)?; + + AstExpr::Calculation { + name: CalculationName::Calc, + args, + } + .span(parser.toks_mut().span_from(start)) + } + "min" | "max" => { + // min() and max() are parsed as calculations if possible, and otherwise + // are parsed as normal Sass functions. + let before_args = parser.toks().cursor(); + + let args = match ValueParser::parse_calculation_arguments(parser, None, start) { + Ok(args) => args, + Err(..) => { + parser.toks_mut().set_cursor(before_args); + return Ok(None); + } + }; + + AstExpr::Calculation { + name: if name == "min" { + CalculationName::Min + } else { + CalculationName::Max + }, + args, + } + .span(parser.toks_mut().span_from(start)) + } + "clamp" => { + let args = ValueParser::parse_calculation_arguments(parser, Some(3), start)?; + AstExpr::Calculation { + name: CalculationName::Clamp, + args, + } + .span(parser.toks_mut().span_from(start)) + } + _ => return Ok(None), + })) + } + + fn reset_state(&mut self, parser: &mut P) -> SassResult<()> { + self.comma_expressions = None; + self.space_expressions = None; + self.binary_operators = None; + self.operands = None; + parser.toks_mut().set_cursor(self.start); + self.allow_slash = true; + self.single_expression = Some(self.parse_single_expression(parser)?); + + Ok(()) + } +} diff --git a/src/parse/value/css_function.rs b/src/parse/value/css_function.rs deleted file mode 100644 index 58168fc7..00000000 --- a/src/parse/value/css_function.rs +++ /dev/null @@ -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 { - 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> { - 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> { - 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> { - 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 { - 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) - } -} diff --git a/src/parse/value/eval.rs b/src/parse/value/eval.rs deleted file mode 100644 index 10ea34cf..00000000 --- a/src/parse/value/eval.rs +++ /dev/null @@ -1,1018 +0,0 @@ -#![allow(unused_variables)] - -use std::cmp::Ordering; - -use codemap::{Span, Spanned}; -use num_traits::Zero; - -use crate::{ - args::CallArgs, - common::{Identifier, Op, QuoteKind}, - error::SassResult, - unit::Unit, - value::{SassFunction, Value}, -}; - -use super::super::Parser; - -#[derive(Clone, Debug)] -pub(crate) enum HigherIntermediateValue { - Literal(Value), - /// A function that hasn't yet been evaluated - Function(SassFunction, CallArgs, Option>), - BinaryOp(Box, Op, Box), - UnaryOp(Op, Box), -} - -impl HigherIntermediateValue { - pub const fn span(self, span: Span) -> Spanned { - Spanned { node: self, span } - } -} - -impl<'a, 'b> Parser<'a, 'b> { - fn call_function( - &mut self, - function: SassFunction, - args: CallArgs, - module: Option>, - ) -> SassResult { - function.call(args, module, self) - } -} - -pub(crate) struct ValueVisitor<'a, 'b: 'a, 'c> { - parser: &'a mut Parser<'b, 'c>, - span: Span, -} - -impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> { - pub fn new(parser: &'a mut Parser<'b, 'c>, span: Span) -> Self { - Self { parser, span } - } - - pub fn eval(&mut self, value: HigherIntermediateValue, in_parens: bool) -> SassResult { - match value { - HigherIntermediateValue::Literal(Value::Dimension(n, u, _)) if in_parens => { - Ok(Value::Dimension(n, u, true)) - } - HigherIntermediateValue::Literal(v) => Ok(v), - HigherIntermediateValue::BinaryOp(v1, op, v2) => self.bin_op(*v1, op, *v2, in_parens), - HigherIntermediateValue::UnaryOp(op, val) => self.unary_op(op, *val, in_parens), - HigherIntermediateValue::Function(function, args, module) => { - self.parser.call_function(function, args, module) - } - } - } - - fn bin_op_one_level( - &mut self, - val1: HigherIntermediateValue, - op: Op, - val2: HigherIntermediateValue, - in_parens: bool, - ) -> SassResult { - let val1 = self.unary(val1, in_parens)?; - let val2 = self.unary(val2, in_parens)?; - - let val1 = match val1 { - HigherIntermediateValue::Literal(val1) => val1, - HigherIntermediateValue::BinaryOp(val1_1, val1_op, val1_2) => { - if val1_op.precedence() >= op.precedence() { - return Ok(HigherIntermediateValue::BinaryOp( - Box::new(self.bin_op_one_level(*val1_1, val1_op, *val1_2, in_parens)?), - op, - Box::new(val2), - )); - } - - return Ok(HigherIntermediateValue::BinaryOp( - val1_1, - val1_op, - Box::new(self.bin_op_one_level(*val1_2, op, val2, in_parens)?), - )); - } - _ => unreachable!(), - }; - - let val2 = match val2 { - HigherIntermediateValue::Literal(val2) => val2, - HigherIntermediateValue::BinaryOp(val2_1, val2_op, val2_2) => { - todo!() - } - _ => unreachable!(), - }; - - let val1 = HigherIntermediateValue::Literal(val1); - let val2 = HigherIntermediateValue::Literal(val2); - - Ok(HigherIntermediateValue::Literal(match op { - Op::Plus => self.add(val1, val2)?, - Op::Minus => self.sub(val1, val2)?, - Op::Mul => self.mul(val1, val2)?, - Op::Div => self.div(val1, val2, in_parens)?, - Op::Rem => self.rem(val1, val2)?, - Op::And => Self::and(val1, val2), - Op::Or => Self::or(val1, val2), - Op::Equal => Self::equal(val1, val2), - Op::NotEqual => Self::not_equal(val1, val2), - Op::GreaterThan => self.greater_than(val1, val2)?, - Op::GreaterThanEqual => self.greater_than_or_equal(val1, val2)?, - Op::LessThan => self.less_than(val1, val2)?, - Op::LessThanEqual => self.less_than_or_equal(val1, val2)?, - Op::Not => unreachable!(), - })) - } - - fn bin_op( - &mut self, - val1: HigherIntermediateValue, - op: Op, - val2: HigherIntermediateValue, - in_parens: bool, - ) -> SassResult { - let mut val1 = self.unary(val1, in_parens)?; - let mut val2 = self.unary(val2, in_parens)?; - - if let HigherIntermediateValue::BinaryOp(val1_1, val1_op, val1_2) = val1 { - let in_parens = op != Op::Div || val1_op != Op::Div; - - return if val1_op.precedence() >= op.precedence() { - val1 = self.bin_op_one_level(*val1_1, val1_op, *val1_2, in_parens)?; - self.bin_op(val1, op, val2, in_parens) - } else { - val2 = self.bin_op_one_level(*val1_2, op, val2, in_parens)?; - self.bin_op(*val1_1, val1_op, val2, in_parens) - }; - } - - Ok(match op { - Op::Plus => self.add(val1, val2)?, - Op::Minus => self.sub(val1, val2)?, - Op::Mul => self.mul(val1, val2)?, - Op::Div => self.div(val1, val2, in_parens)?, - Op::Rem => self.rem(val1, val2)?, - Op::And => Self::and(val1, val2), - Op::Or => Self::or(val1, val2), - Op::Equal => Self::equal(val1, val2), - Op::NotEqual => Self::not_equal(val1, val2), - Op::GreaterThan => self.greater_than(val1, val2)?, - Op::GreaterThanEqual => self.greater_than_or_equal(val1, val2)?, - Op::LessThan => self.less_than(val1, val2)?, - Op::LessThanEqual => self.less_than_or_equal(val1, val2)?, - Op::Not => unreachable!(), - }) - } - - fn unary_op( - &mut self, - op: Op, - val: HigherIntermediateValue, - in_parens: bool, - ) -> SassResult { - let val = self.eval(val, in_parens)?; - match op { - Op::Minus => self.unary_minus(val), - Op::Not => Ok(Self::unary_not(&val)), - Op::Plus => self.unary_plus(val), - _ => unreachable!(), - } - } - - fn unary_minus(&self, val: Value) -> SassResult { - Ok(match val { - Value::Dimension(Some(n), u, should_divide) => { - Value::Dimension(Some(-n), u, should_divide) - } - Value::Dimension(None, u, should_divide) => Value::Dimension(None, u, should_divide), - v => Value::String( - format!( - "-{}", - v.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }) - } - - fn unary_plus(&self, val: Value) -> SassResult { - Ok(match val { - v @ Value::Dimension(..) => v, - v => Value::String( - format!( - "+{}", - v.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }) - } - - fn unary_not(val: &Value) -> Value { - Value::bool(!val.is_true()) - } - - fn unary( - &mut self, - val: HigherIntermediateValue, - in_parens: bool, - ) -> SassResult { - Ok(match val { - HigherIntermediateValue::UnaryOp(op, val) => { - HigherIntermediateValue::Literal(self.unary_op(op, *val, in_parens)?) - } - HigherIntermediateValue::Function(function, args, module) => { - HigherIntermediateValue::Literal(self.parser.call_function(function, args, module)?) - } - _ => val, - }) - } - - fn add( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Ok(match left { - Value::Map(..) | Value::FunctionRef(..) => { - return Err(( - format!("{} isn't a valid CSS value.", left.inspect(self.span)?), - self.span, - ) - .into()) - } - Value::True | Value::False => match right { - Value::String(s, QuoteKind::Quoted) => Value::String( - format!( - "{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - s - ), - QuoteKind::Quoted, - ), - _ => Value::String( - format!( - "{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - Value::Important => match right { - Value::String(s, ..) => Value::String( - format!( - "{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - s - ), - QuoteKind::None, - ), - _ => Value::String( - format!( - "{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - Value::Null => match right { - Value::Null => Value::Null, - _ => Value::String( - right - .to_css_string(self.span, self.parser.options.is_compressed())? - .into_owned(), - QuoteKind::None, - ), - }, - v @ Value::Dimension(None, ..) => v, - Value::Dimension(Some(num), unit, _) => match right { - v @ Value::Dimension(None, ..) => v, - Value::Dimension(Some(num2), unit2, _) => { - if !unit.comparable(&unit2) { - return Err(( - format!("Incompatible units {} and {}.", unit2, unit), - self.span, - ) - .into()); - } - if unit == unit2 { - Value::Dimension(Some(num + num2), unit, true) - } else if unit == Unit::None { - Value::Dimension(Some(num + num2), unit2, true) - } else if unit2 == Unit::None { - Value::Dimension(Some(num + num2), unit, true) - } else { - Value::Dimension(Some(num + num2.convert(&unit2, &unit)), unit, true) - } - } - Value::String(s, q) => Value::String( - format!( - "{}{}{}", - num.to_string(self.parser.options.is_compressed()), - unit, - s - ), - q, - ), - Value::Null => Value::String( - format!( - "{}{}", - num.to_string(self.parser.options.is_compressed()), - unit - ), - QuoteKind::None, - ), - Value::True - | Value::False - | Value::List(..) - | Value::Important - | Value::ArgList(..) => Value::String( - format!( - "{}{}{}", - num.to_string(self.parser.options.is_compressed()), - unit, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - Value::Map(..) | Value::FunctionRef(..) => { - return Err(( - format!("{} isn't a valid CSS value.", right.inspect(self.span)?), - self.span, - ) - .into()) - } - Value::Color(..) => { - return Err(( - format!( - "Undefined operation \"{}{} + {}\".", - num.inspect(), - unit, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }, - Value::Color(c) => match right { - Value::String(s, q) => Value::String(format!("{}{}", c, s), q), - Value::Null => Value::String(c.to_string(), QuoteKind::None), - Value::List(..) => Value::String( - format!( - "{}{}", - c, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - _ => { - return Err(( - format!( - "Undefined operation \"{} + {}\".", - c, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }, - Value::String(text, quotes) => match right { - Value::String(text2, ..) => Value::String(text + &text2, quotes), - _ => Value::String( - text + &right.to_css_string(self.span, self.parser.options.is_compressed())?, - quotes, - ), - }, - Value::List(..) | Value::ArgList(..) => match right { - Value::String(s, q) => Value::String( - format!( - "{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - s - ), - q, - ), - _ => Value::String( - format!( - "{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - }) - } - - fn sub( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Ok(match left { - Value::Null => Value::String( - format!( - "-{}", - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - v @ Value::Dimension(None, ..) => v, - Value::Dimension(Some(num), unit, _) => match right { - v @ Value::Dimension(None, ..) => v, - Value::Dimension(Some(num2), unit2, _) => { - if !unit.comparable(&unit2) { - return Err(( - format!("Incompatible units {} and {}.", unit2, unit), - self.span, - ) - .into()); - } - if unit == unit2 { - Value::Dimension(Some(num - num2), unit, true) - } else if unit == Unit::None { - Value::Dimension(Some(num - num2), unit2, true) - } else if unit2 == Unit::None { - Value::Dimension(Some(num - num2), unit, true) - } else { - Value::Dimension(Some(num - num2.convert(&unit2, &unit)), unit, true) - } - } - Value::List(..) - | Value::String(..) - | Value::Important - | Value::True - | Value::False - | Value::ArgList(..) => Value::String( - format!( - "{}{}-{}", - num.to_string(self.parser.options.is_compressed()), - unit, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - Value::Map(..) | Value::FunctionRef(..) => { - return Err(( - format!("{} isn't a valid CSS value.", right.inspect(self.span)?), - self.span, - ) - .into()) - } - Value::Color(..) => { - return Err(( - format!( - "Undefined operation \"{}{} - {}\".", - num.inspect(), - unit, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - Value::Null => Value::String( - format!( - "{}{}-", - num.to_string(self.parser.options.is_compressed()), - unit - ), - QuoteKind::None, - ), - }, - Value::Color(c) => match right { - Value::String(s, q) => { - Value::String(format!("{}-{}{}{}", c, q, s, q), QuoteKind::None) - } - Value::Null => Value::String(format!("{}-", c), QuoteKind::None), - Value::Dimension(..) | Value::Color(..) => { - return Err(( - format!( - "Undefined operation \"{} - {}\".", - c, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - _ => Value::String( - format!( - "{}-{}", - c, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - Value::String(..) => Value::String( - format!( - "{}-{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - _ => match right { - Value::String(s, q) => Value::String( - format!( - "{}-{}{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - q, - s, - q - ), - QuoteKind::None, - ), - Value::Null => Value::String( - format!( - "{}-", - left.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - _ => Value::String( - format!( - "{}-{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - }) - } - - fn mul( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Ok(match left { - Value::Dimension(None, ..) => todo!(), - Value::Dimension(Some(num), unit, _) => match right { - Value::Dimension(None, ..) => todo!(), - Value::Dimension(Some(num2), unit2, _) => { - if unit == Unit::None { - Value::Dimension(Some(num * num2), unit2, true) - } else if unit2 == Unit::None { - Value::Dimension(Some(num * num2), unit, true) - } else { - Value::Dimension(Some(num * num2), unit * unit2, true) - } - } - _ => { - return Err(( - format!( - "Undefined operation \"{}{} * {}\".", - num.inspect(), - unit, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }, - _ => { - return Err(( - format!( - "Undefined operation \"{} * {}\".", - left.inspect(self.span)?, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }) - } - - fn div( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - in_parens: bool, - ) -> SassResult { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Ok(match left { - Value::Null => Value::String( - format!( - "/{}", - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - Value::Dimension(None, ..) => todo!(), - Value::Dimension(Some(num), unit, should_divide1) => match right { - Value::Dimension(None, ..) => todo!(), - Value::Dimension(Some(num2), unit2, should_divide2) => { - if should_divide1 || should_divide2 || in_parens { - if num.is_zero() && num2.is_zero() { - return Ok(Value::Dimension(None, Unit::None, true)); - } - - if num2.is_zero() { - // todo: Infinity and -Infinity - return Err(("Infinity not yet implemented.", self.span).into()); - } - - // `unit(1em / 1em)` => `""` - if unit == unit2 { - Value::Dimension(Some(num / num2), Unit::None, true) - - // `unit(1 / 1em)` => `"em^-1"` - } else if unit == Unit::None { - Value::Dimension(Some(num / num2), Unit::None / unit2, true) - - // `unit(1em / 1)` => `"em"` - } else if unit2 == Unit::None { - Value::Dimension(Some(num / num2), unit, true) - - // `unit(1in / 1px)` => `""` - } else if unit.comparable(&unit2) { - Value::Dimension( - Some(num / num2.convert(&unit2, &unit)), - Unit::None, - true, - ) - // `unit(1em / 1px)` => `"em/px"` - // todo: this should probably be its own variant - // within the `Value` enum - } else { - // todo: remember to account for `Mul` and `Div` - // todo!("non-comparable inverse units") - return Err(( - "Division of non-comparable units not yet supported.", - self.span, - ) - .into()); - } - } else { - Value::String( - format!( - "{}{}/{}{}", - num.to_string(self.parser.options.is_compressed()), - unit, - num2.to_string(self.parser.options.is_compressed()), - unit2 - ), - QuoteKind::None, - ) - } - } - Value::String(s, q) => Value::String( - format!( - "{}{}/{}{}{}", - num.to_string(self.parser.options.is_compressed()), - unit, - q, - s, - q - ), - QuoteKind::None, - ), - Value::List(..) - | Value::True - | Value::False - | Value::Important - | Value::Color(..) - | Value::ArgList(..) => Value::String( - format!( - "{}{}/{}", - num.to_string(self.parser.options.is_compressed()), - unit, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - Value::Null => Value::String( - format!( - "{}{}/", - num.to_string(self.parser.options.is_compressed()), - unit - ), - QuoteKind::None, - ), - Value::Map(..) | Value::FunctionRef(..) => { - return Err(( - format!("{} isn't a valid CSS value.", right.inspect(self.span)?), - self.span, - ) - .into()) - } - }, - Value::Color(c) => match right { - Value::String(s, q) => { - Value::String(format!("{}/{}{}{}", c, q, s, q), QuoteKind::None) - } - Value::Null => Value::String(format!("{}/", c), QuoteKind::None), - Value::Dimension(..) | Value::Color(..) => { - return Err(( - format!( - "Undefined operation \"{} / {}\".", - c, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - _ => Value::String( - format!( - "{}/{}", - c, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - Value::String(s1, q1) => match right { - Value::String(s2, q2) => Value::String( - format!("{}{}{}/{}{}{}", q1, s1, q1, q2, s2, q2), - QuoteKind::None, - ), - Value::Important - | Value::True - | Value::False - | Value::Dimension(..) - | Value::Color(..) - | Value::List(..) - | Value::ArgList(..) => Value::String( - format!( - "{}{}{}/{}", - q1, - s1, - q1, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - Value::Null => Value::String(format!("{}{}{}/", q1, s1, q1), QuoteKind::None), - Value::Map(..) | Value::FunctionRef(..) => { - return Err(( - format!("{} isn't a valid CSS value.", right.inspect(self.span)?), - self.span, - ) - .into()) - } - }, - _ => match right { - Value::String(s, q) => Value::String( - format!( - "{}/{}{}{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - q, - s, - q - ), - QuoteKind::None, - ), - Value::Null => Value::String( - format!( - "{}/", - left.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - _ => Value::String( - format!( - "{}/{}", - left.to_css_string(self.span, self.parser.options.is_compressed())?, - right.to_css_string(self.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - ), - }, - }) - } - - fn rem( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Ok(match left { - v @ Value::Dimension(None, ..) => v, - Value::Dimension(Some(n), u, _) => match right { - v @ Value::Dimension(None, ..) => v, - Value::Dimension(Some(n2), u2, _) => { - if !u.comparable(&u2) { - return Err( - (format!("Incompatible units {} and {}.", u, u2), self.span).into() - ); - } - - if n2.is_zero() { - return Ok(Value::Dimension( - None, - if u == Unit::None { u2 } else { u }, - true, - )); - } - - if u == u2 { - Value::Dimension(Some(n % n2), u, true) - } else if u == Unit::None { - Value::Dimension(Some(n % n2), u2, true) - } else if u2 == Unit::None { - Value::Dimension(Some(n % n2), u, true) - } else { - Value::Dimension(Some(n), u, true) - } - } - _ => { - return Err(( - format!( - "Undefined operation \"{} % {}\".", - Value::Dimension(Some(n), u, true).inspect(self.span)?, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }, - _ => { - return Err(( - format!( - "Undefined operation \"{} % {}\".", - left.inspect(self.span)?, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }) - } - - fn and(left: HigherIntermediateValue, right: HigherIntermediateValue) -> Value { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - if left.is_true() { - right - } else { - left - } - } - - fn or(left: HigherIntermediateValue, right: HigherIntermediateValue) -> Value { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - if left.is_true() { - left - } else { - right - } - } - - pub fn equal(left: HigherIntermediateValue, right: HigherIntermediateValue) -> Value { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Value::bool(left == right) - } - - fn not_equal(left: HigherIntermediateValue, right: HigherIntermediateValue) -> Value { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - Value::bool(left.not_equals(&right)) - } - - fn cmp( - &self, - left: HigherIntermediateValue, - op: Op, - right: HigherIntermediateValue, - ) -> SassResult { - let left = match left { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - let right = match right { - HigherIntermediateValue::Literal(v) => v, - v => panic!("{:?}", v), - }; - - let ordering = left.cmp(&right, self.span, op)?; - - Ok(match op { - Op::GreaterThan => match ordering { - Ordering::Greater => Value::True, - Ordering::Less | Ordering::Equal => Value::False, - }, - Op::GreaterThanEqual => match ordering { - Ordering::Greater | Ordering::Equal => Value::True, - Ordering::Less => Value::False, - }, - Op::LessThan => match ordering { - Ordering::Less => Value::True, - Ordering::Greater | Ordering::Equal => Value::False, - }, - Op::LessThanEqual => match ordering { - Ordering::Less | Ordering::Equal => Value::True, - Ordering::Greater => Value::False, - }, - _ => unreachable!(), - }) - } - - pub fn greater_than( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - self.cmp(left, Op::GreaterThan, right) - } - - fn greater_than_or_equal( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - self.cmp(left, Op::GreaterThanEqual, right) - } - - pub fn less_than( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - self.cmp(left, Op::LessThan, right) - } - - fn less_than_or_equal( - &self, - left: HigherIntermediateValue, - right: HigherIntermediateValue, - ) -> SassResult { - self.cmp(left, Op::LessThanEqual, right) - } -} diff --git a/src/parse/value/mod.rs b/src/parse/value/mod.rs deleted file mode 100644 index d2de5264..00000000 --- a/src/parse/value/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub(crate) use eval::{HigherIntermediateValue, ValueVisitor}; - -mod css_function; -mod eval; -mod parse; diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs deleted file mode 100644 index f5977b0c..00000000 --- a/src/parse/value/parse.rs +++ /dev/null @@ -1,1420 +0,0 @@ -use std::{iter::Iterator, mem}; - -use num_bigint::BigInt; -use num_rational::{BigRational, Rational64}; -use num_traits::{pow, One, ToPrimitive}; - -use codemap::{Span, Spanned}; - -use crate::{ - builtin::GLOBAL_FUNCTIONS, - color::{Color, NAMED_COLORS}, - common::{unvendor, Brackets, Identifier, ListSeparator, Op, QuoteKind}, - error::SassResult, - lexer::Lexer, - unit::Unit, - utils::{is_name, IsWhitespace, ParsedNumber}, - value::{Number, SassFunction, SassMap, Value}, - Token, -}; - -use super::eval::{HigherIntermediateValue, ValueVisitor}; - -use super::super::Parser; - -#[derive(Clone, Debug)] -enum IntermediateValue { - Value(HigherIntermediateValue), - Op(Op), - Comma, - Whitespace, -} - -impl IntermediateValue { - const fn span(self, span: Span) -> Spanned { - Spanned { node: self, span } - } -} - -impl IsWhitespace for IntermediateValue { - fn is_whitespace(&self) -> bool { - if let IntermediateValue::Whitespace = self { - return true; - } - false - } -} - -/// We parse a value until the predicate returns true -type Predicate<'a> = &'a dyn Fn(&mut Parser<'_, '_>) -> bool; - -impl<'a, 'b> Parser<'a, 'b> { - /// Parse a value from a stream of tokens - /// - /// This function will cease parsing if the predicate returns true. - pub(crate) fn parse_value( - &mut self, - in_paren: bool, - predicate: Predicate<'_>, - ) -> SassResult> { - self.whitespace(); - - let span = match self.toks.peek() { - Some(Token { kind: '}', .. }) - | Some(Token { kind: ';', .. }) - | Some(Token { kind: '{', .. }) - | None => return Err(("Expected expression.", self.span_before).into()), - Some(Token { pos, .. }) => pos, - }; - - if predicate(self) { - return Err(("Expected expression.", span).into()); - } - - let mut last_was_whitespace = false; - let mut space_separated = Vec::new(); - let mut comma_separated = Vec::new(); - let mut iter = IntermediateValueIterator::new(self, &predicate); - while let Some(val) = iter.next() { - let val = val?; - match val.node { - IntermediateValue::Value(v) => { - last_was_whitespace = false; - space_separated.push(v.span(val.span)); - } - IntermediateValue::Op(op) => { - iter.parse_op( - Spanned { - node: op, - span: val.span, - }, - &mut space_separated, - last_was_whitespace, - in_paren, - )?; - } - IntermediateValue::Whitespace => { - last_was_whitespace = true; - continue; - } - IntermediateValue::Comma => { - last_was_whitespace = false; - - if space_separated.len() == 1 { - comma_separated.push(space_separated.pop().unwrap()); - } else { - let mut span = space_separated - .first() - .ok_or(("Expected expression.", val.span))? - .span; - comma_separated.push( - HigherIntermediateValue::Literal(Value::List( - mem::take(&mut space_separated) - .into_iter() - .map(move |a| { - span = span.merge(a.span); - a.node - }) - .map(|a| ValueVisitor::new(iter.parser, span).eval(a, in_paren)) - .collect::>>()?, - ListSeparator::Space, - Brackets::None, - )) - .span(span), - ); - } - } - } - } - - Ok(if !comma_separated.is_empty() { - if space_separated.len() == 1 { - comma_separated.push(space_separated.pop().unwrap()); - } else if !space_separated.is_empty() { - comma_separated.push( - HigherIntermediateValue::Literal(Value::List( - space_separated - .into_iter() - .map(|a| ValueVisitor::new(self, span).eval(a.node, in_paren)) - .collect::>>()?, - ListSeparator::Space, - Brackets::None, - )) - .span(span), - ); - } - Value::List( - comma_separated - .into_iter() - .map(|a| ValueVisitor::new(self, span).eval(a.node, in_paren)) - .collect::>>()?, - ListSeparator::Comma, - Brackets::None, - ) - .span(span) - } else if space_separated.len() == 1 { - ValueVisitor::new(self, span) - .eval(space_separated.pop().unwrap().node, in_paren)? - .span(span) - } else { - Value::List( - space_separated - .into_iter() - .map(|a| ValueVisitor::new(self, span).eval(a.node, in_paren)) - .collect::>>()?, - ListSeparator::Space, - Brackets::None, - ) - .span(span) - }) - } - - pub(crate) fn parse_value_from_vec( - &mut self, - toks: &[Token], - in_paren: bool, - ) -> SassResult> { - Parser { - toks: &mut Lexer::new_ref(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_value(in_paren, &|_| false) - } - - #[allow(clippy::eval_order_dependence)] - fn parse_module_item( - &mut self, - mut module: Spanned, - ) -> SassResult> { - let is_var_start = self.consume_char_if_exists('$'); - - let var_or_fn_name = self - .parse_identifier_no_interpolation(false)? - .map_node(Into::into); - - let value = if is_var_start { - module.span = module.span.merge(var_or_fn_name.span); - - let value = self - .modules - .get(module.node, module.span)? - .get_var(var_or_fn_name)?; - - HigherIntermediateValue::Literal(value.clone()) - } else { - let function = self - .modules - .get(module.node, module.span)? - .get_fn(var_or_fn_name)? - .ok_or(("Undefined function.", var_or_fn_name.span))?; - - self.expect_char('(')?; - - let call_args = self.parse_call_args()?; - - HigherIntermediateValue::Function(function, call_args, Some(module)) - }; - - Ok(IntermediateValue::Value(value).span(module.span)) - } - - fn parse_fn_call( - &mut self, - mut s: String, - lower: String, - ) -> SassResult> { - if lower == "min" || lower == "max" { - let start = self.toks.cursor(); - match self.try_parse_min_max(&lower, true)? { - Some(val) => { - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::String(val, QuoteKind::None), - )) - .span(self.span_before)); - } - None => { - self.toks.set_cursor(start); - } - } - } - - let as_ident = Identifier::from(&s); - let func = match self.scopes.get_fn(as_ident, self.global_scope) { - Some(f) => f, - None => { - if let Some(f) = GLOBAL_FUNCTIONS.get(as_ident.as_str()) { - return Ok(IntermediateValue::Value(HigherIntermediateValue::Function( - SassFunction::Builtin(f.clone(), as_ident), - self.parse_call_args()?, - None, - )) - .span(self.span_before)); - } - - // check for special cased CSS functions - match unvendor(&lower) { - "calc" | "element" | "expression" => { - s = lower; - self.parse_calc_args(&mut s)?; - } - "url" => match self.try_parse_url()? { - Some(val) => s = val, - None => s.push_str( - &self - .parse_call_args()? - .to_css_string(self.options.is_compressed())?, - ), - }, - "clamp" if lower == "clamp" => { - self.parse_calc_args(&mut s)?; - } - _ => s.push_str( - &self - .parse_call_args()? - .to_css_string(self.options.is_compressed())?, - ), - } - - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::String(s, QuoteKind::None), - )) - .span(self.span_before)); - } - }; - - let call_args = self.parse_call_args()?; - Ok( - IntermediateValue::Value(HigherIntermediateValue::Function(func, call_args, None)) - .span(self.span_before), - ) - } - - fn parse_ident_value( - &mut self, - predicate: Predicate<'_>, - ) -> SassResult> { - let Spanned { node: mut s, span } = self.parse_identifier()?; - - self.span_before = span; - - let lower = s.to_ascii_lowercase(); - - if lower == "progid" && self.consume_char_if_exists(':') { - s = lower; - s.push(':'); - s.push_str(&self.parse_progid()?); - return Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(Value::String( - s, - QuoteKind::None, - ))), - span, - }); - } - - if !is_keyword_operator(&s) { - match self.toks.peek() { - Some(Token { kind: '(', .. }) => { - self.span_before = span; - self.toks.next(); - - return self.parse_fn_call(s, lower); - } - Some(Token { kind: '.', .. }) => { - if !predicate(self) { - self.toks.next(); - return self.parse_module_item(Spanned { - node: s.into(), - span, - }); - } - } - _ => {} - } - } - - // check for named colors - Ok(if let Some(c) = NAMED_COLORS.get_by_name(lower.as_str()) { - IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Color(Box::new( - Color::new(c[0], c[1], c[2], c[3], s), - )))) - } else { - // check for keywords - match s.as_str() { - "true" => IntermediateValue::Value(HigherIntermediateValue::Literal(Value::True)), - "false" => IntermediateValue::Value(HigherIntermediateValue::Literal(Value::False)), - "null" => IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Null)), - "not" => IntermediateValue::Op(Op::Not), - "and" => IntermediateValue::Op(Op::And), - "or" => IntermediateValue::Op(Op::Or), - _ => IntermediateValue::Value(HigherIntermediateValue::Literal(Value::String( - s, - QuoteKind::None, - ))), - } - } - .span(span)) - } - - fn next_is_hypen(&mut self) -> bool { - if let Some(Token { kind, .. }) = self.toks.peek_forward(1) { - matches!(kind, '-' | '_' | 'a'..='z' | 'A'..='Z') - } else { - false - } - } - - pub(crate) fn parse_whole_number(&mut self) -> String { - let mut buf = String::new(); - - while let Some(c) = self.toks.peek() { - if !c.kind.is_ascii_digit() { - break; - } - - let tok = self.toks.next().unwrap(); - buf.push(tok.kind); - } - - buf - } - - fn parse_number(&mut self, predicate: Predicate<'_>) -> SassResult> { - let mut span = self.toks.peek().unwrap().pos; - let mut whole = self.parse_whole_number(); - - if self.toks.peek().is_none() || predicate(self) { - return Ok(Spanned { - node: ParsedNumber::new(whole, 0, String::new(), true), - span, - }); - } - - let next_tok = self.toks.peek().unwrap(); - - let dec_len = if next_tok.kind == '.' { - self.toks.next(); - - let dec = self.parse_whole_number(); - if dec.is_empty() { - return Err(("Expected digit.", next_tok.pos()).into()); - } - - whole.push_str(&dec); - - dec.len() - } else { - 0 - }; - - let mut times_ten = String::new(); - let mut times_ten_is_postive = true; - - if let Some(Token { kind: 'e', .. }) | Some(Token { kind: 'E', .. }) = self.toks.peek() { - if let Some(tok) = self.toks.peek_next() { - match tok.kind { - '-' => { - self.toks.next(); - self.toks.next(); - times_ten_is_postive = false; - - times_ten = self.parse_whole_number(); - - if times_ten.is_empty() { - return Err( - ("Expected digit.", self.toks.peek().unwrap_or(tok).pos).into() - ); - } else if times_ten.len() > 2 { - return Err(( - "Exponent too negative.", - self.toks.peek().unwrap_or(tok).pos, - ) - .into()); - } - } - '0'..='9' => { - self.toks.next(); - times_ten = self.parse_whole_number(); - - if times_ten.len() > 2 { - return Err(( - "Exponent too large.", - self.toks.peek().unwrap_or(tok).pos, - ) - .into()); - } - } - _ => {} - } - } - } - - if let Some(Token { pos, .. }) = self.toks.peek_previous() { - span = span.merge(pos); - } - - self.toks.reset_cursor(); - - Ok(Spanned { - node: ParsedNumber::new(whole, dec_len, times_ten, times_ten_is_postive), - span, - }) - } - - fn parse_bracketed_list(&mut self) -> SassResult> { - let mut span = self.span_before; - self.toks.next(); - self.whitespace_or_comment(); - - Ok(if let Some(Token { kind: ']', pos }) = self.toks.peek() { - span = span.merge(pos); - self.toks.next(); - IntermediateValue::Value(HigherIntermediateValue::Literal(Value::List( - Vec::new(), - ListSeparator::Space, - Brackets::Bracketed, - ))) - .span(span) - } else { - // todo: we don't know if we're `in_paren` here - let inner = self.parse_value(false, &|parser| { - matches!(parser.toks.peek(), Some(Token { kind: ']', .. })) - })?; - - span = span.merge(inner.span); - - self.expect_char(']')?; - - IntermediateValue::Value(HigherIntermediateValue::Literal(match inner.node { - Value::List(els, sep, Brackets::None) => Value::List(els, sep, Brackets::Bracketed), - v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed), - })) - .span(span) - }) - } - - fn parse_intermediate_value_dimension( - &mut self, - predicate: Predicate<'_>, - ) -> SassResult> { - let Spanned { node, span } = self.parse_dimension(predicate)?; - - Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(node)).span(span)) - } - - pub(crate) fn parse_dimension( - &mut self, - predicate: Predicate<'_>, - ) -> SassResult> { - let Spanned { - node: val, - mut span, - } = self.parse_number(predicate)?; - let unit = if let Some(tok) = self.toks.peek() { - let Token { kind, .. } = tok; - match kind { - 'a'..='z' | 'A'..='Z' | '_' | '\\' | '\u{7f}'..=std::char::MAX => { - let u = self.parse_identifier_no_interpolation(true)?; - span = span.merge(u.span); - Unit::from(u.node) - } - '-' => { - let next_token = self.toks.peek_next(); - self.toks.reset_cursor(); - - if let Some(Token { kind, .. }) = next_token { - if matches!(kind, 'a'..='z' | 'A'..='Z' | '_' | '\\' | '\u{7f}'..=std::char::MAX) - { - let u = self.parse_identifier_no_interpolation(true)?; - span = span.merge(u.span); - Unit::from(u.node) - } else { - Unit::None - } - } else { - Unit::None - } - } - '%' => { - span = span.merge(self.toks.next().unwrap().pos()); - Unit::Percent - } - _ => Unit::None, - } - } else { - Unit::None - }; - - let n = if val.dec_len == 0 { - if val.num.len() <= 18 && val.times_ten.is_empty() { - let n = Rational64::new_raw(parse_i64(&val.num), 1); - return Ok(Value::Dimension(Some(Number::new_small(n)), unit, false).span(span)); - } - BigRational::new_raw(val.num.parse::().unwrap(), BigInt::one()) - } else { - if val.num.len() <= 18 && val.times_ten.is_empty() { - let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len)); - return Ok(Value::Dimension(Some(Number::new_small(n)), unit, false).span(span)); - } - BigRational::new(val.num.parse().unwrap(), pow(BigInt::from(10), val.dec_len)) - }; - - if val.times_ten.is_empty() { - return Ok(Value::Dimension(Some(Number::new_big(n)), unit, false).span(span)); - } - - let times_ten = pow( - BigInt::from(10), - val.times_ten - .parse::() - .unwrap() - .to_usize() - .ok_or(("Exponent too large (expected usize).", span))?, - ); - - let times_ten = if val.times_ten_is_postive { - BigRational::new_raw(times_ten, BigInt::one()) - } else { - BigRational::new(BigInt::one(), times_ten) - }; - - Ok(Value::Dimension(Some(Number::new_big(n * times_ten)), unit, false).span(span)) - } - - fn parse_paren(&mut self) -> SassResult> { - if self.consume_char_if_exists(')') { - return Ok( - IntermediateValue::Value(HigherIntermediateValue::Literal(Value::List( - Vec::new(), - ListSeparator::Space, - Brackets::None, - ))) - .span(self.span_before), - ); - } - - let mut map = SassMap::new(); - let key = self.parse_value(true, &|parser| { - matches!( - parser.toks.peek(), - Some(Token { kind: ':', .. }) | Some(Token { kind: ')', .. }) - ) - })?; - - match self.toks.next() { - Some(Token { kind: ':', .. }) => {} - Some(Token { kind: ')', .. }) => { - return Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(key.node)), - span: key.span, - }); - } - Some(..) | None => return Err(("expected \")\".", key.span).into()), - } - - let val = self.parse_value(true, &|parser| { - matches!( - parser.toks.peek(), - Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. }) - ) - })?; - - map.insert(key.node, val.node); - - let mut span = key.span.merge(val.span); - - match self.toks.next() { - Some(Token { kind: ',', .. }) => {} - Some(Token { kind: ')', .. }) => { - return Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Map( - map, - ))), - span, - }); - } - Some(..) | None => return Err(("expected \")\".", key.span).into()), - } - - self.whitespace_or_comment(); - - while self.consume_char_if_exists(',') { - self.whitespace_or_comment(); - } - - if self.consume_char_if_exists(')') { - return Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Map(map))), - span, - }); - } - - loop { - let key = self.parse_value(true, &|parser| { - matches!( - parser.toks.peek(), - Some(Token { kind: ':', .. }) | Some(Token { kind: ',', .. }) - ) - })?; - - self.expect_char(':')?; - - self.whitespace_or_comment(); - let val = self.parse_value(true, &|parser| { - matches!( - parser.toks.peek(), - Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. }) - ) - })?; - - span = span.merge(val.span); - - if map.insert(key.node.clone(), val.node) { - return Err(("Duplicate key.", key.span).into()); - } - - let found_comma = self.consume_char_if_exists(','); - - self.whitespace_or_comment(); - - match self.toks.peek() { - Some(Token { kind: ')', .. }) => { - self.toks.next(); - break; - } - Some(..) if found_comma => continue, - Some(..) | None => return Err(("expected \")\".", val.span).into()), - } - } - Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Map(map))), - span, - }) - } - - fn in_interpolated_identifier_body(&mut self) -> bool { - match self.toks.peek() { - Some(Token { kind: '\\', .. }) => true, - Some(Token { kind, .. }) if is_name(kind) => true, - Some(Token { kind: '#', .. }) => { - let next_is_curly = matches!(self.toks.peek_next(), Some(Token { kind: '{', .. })); - self.toks.reset_cursor(); - next_is_curly - } - Some(..) | None => false, - } - } - - /// single codepoint: U+26 - /// Codepoint range: U+0-7F - /// Wildcard range: U+4?? - fn parse_unicode_range(&mut self, kind: char) -> SassResult> { - let mut buf = String::with_capacity(4); - let mut span = self.span_before; - buf.push(kind); - buf.push('+'); - - for _ in 0..6 { - if let Some(Token { kind, pos }) = self.toks.peek() { - if kind.is_ascii_hexdigit() { - span = span.merge(pos); - self.span_before = pos; - buf.push(kind); - self.toks.next(); - } else { - break; - } - } - } - - if self.consume_char_if_exists('?') { - buf.push('?'); - for _ in 0..(8_usize.saturating_sub(buf.len())) { - if let Some(Token { kind: '?', pos }) = self.toks.peek() { - span = span.merge(pos); - self.span_before = pos; - buf.push('?'); - self.toks.next(); - } else { - break; - } - } - return Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(Value::String( - buf, - QuoteKind::None, - ))), - span, - }); - } - - if buf.len() == 2 { - return Err(("Expected hex digit or \"?\".", self.span_before).into()); - } - - if self.consume_char_if_exists('-') { - buf.push('-'); - let mut found_hex_digit = false; - for _ in 0..6 { - found_hex_digit = true; - if let Some(Token { kind, pos }) = self.toks.peek() { - if kind.is_ascii_hexdigit() { - span = span.merge(pos); - self.span_before = pos; - buf.push(kind); - self.toks.next(); - } else { - break; - } - } - } - - if !found_hex_digit { - return Err(("Expected hex digit.", self.span_before).into()); - } - } - - if self.in_interpolated_identifier_body() { - return Err(("Expected end of identifier.", self.span_before).into()); - } - - Ok(Spanned { - node: IntermediateValue::Value(HigherIntermediateValue::Literal(Value::String( - buf, - QuoteKind::None, - ))), - span, - }) - } - - fn parse_intermediate_value( - &mut self, - predicate: Predicate<'_>, - ) -> Option>> { - if predicate(self) { - return None; - } - let (kind, span) = match self.toks.peek() { - Some(v) => (v.kind, v.pos()), - None => return None, - }; - - self.span_before = span; - - if self.whitespace() { - return Some(Ok(Spanned { - node: IntermediateValue::Whitespace, - span, - })); - } - - Some(Ok(match kind { - _ if kind.is_ascii_alphabetic() - || kind == '_' - || kind == '\\' - || (!kind.is_ascii() && !kind.is_control()) - || (kind == '-' && self.next_is_hypen()) => - { - if kind == 'U' || kind == 'u' { - if matches!(self.toks.peek_next(), Some(Token { kind: '+', .. })) { - self.toks.next(); - self.toks.next(); - return Some(self.parse_unicode_range(kind)); - } - - self.toks.reset_cursor(); - } - return Some(self.parse_ident_value(predicate)); - } - '0'..='9' | '.' => return Some(self.parse_intermediate_value_dimension(predicate)), - '(' => { - self.toks.next(); - return Some(self.parse_paren()); - } - '&' => { - let span = self.toks.next().unwrap().pos(); - if self.super_selectors.is_empty() - && !self.at_root_has_selector - && !self.flags.in_at_root_rule() - { - IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Null)) - .span(span) - } else { - IntermediateValue::Value(HigherIntermediateValue::Literal( - self.super_selectors - .last() - .clone() - .into_selector() - .into_value(), - )) - .span(span) - } - } - '#' => { - if let Some(Token { kind: '{', pos }) = self.toks.peek_forward(1) { - self.span_before = pos; - self.toks.reset_cursor(); - return Some(self.parse_ident_value(predicate)); - } - self.toks.reset_cursor(); - self.toks.next(); - let hex = match self.parse_hex() { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - IntermediateValue::Value(HigherIntermediateValue::Literal(hex.node)).span(hex.span) - } - q @ '"' | q @ '\'' => { - let span_start = self.toks.next().unwrap().pos(); - let Spanned { node, span } = match self.parse_quoted_string(q) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - IntermediateValue::Value(HigherIntermediateValue::Literal(node)) - .span(span_start.merge(span)) - } - '[' => return Some(self.parse_bracketed_list()), - '$' => { - self.toks.next(); - let val = match self.parse_identifier_no_interpolation(false) { - Ok(v) => v.map_node(Into::into), - Err(e) => return Some(Err(e)), - }; - IntermediateValue::Value(HigherIntermediateValue::Literal( - match self.scopes.get_var(val, self.global_scope) { - Ok(v) => v.clone(), - Err(e) => return Some(Err(e)), - }, - )) - .span(val.span) - } - '+' => { - let span = self.toks.next().unwrap().pos(); - IntermediateValue::Op(Op::Plus).span(span) - } - '-' => { - if matches!(self.toks.peek(), Some(Token { kind: '#', .. })) - && matches!(self.toks.peek_next(), Some(Token { kind: '{', .. })) - { - self.toks.reset_cursor(); - return Some(self.parse_ident_value(predicate)); - } - self.toks.reset_cursor(); - let span = self.toks.next().unwrap().pos(); - IntermediateValue::Op(Op::Minus).span(span) - } - '*' => { - let span = self.toks.next().unwrap().pos(); - IntermediateValue::Op(Op::Mul).span(span) - } - '%' => { - let span = self.toks.next().unwrap().pos(); - IntermediateValue::Op(Op::Rem).span(span) - } - ',' => { - self.toks.next(); - IntermediateValue::Comma.span(span) - } - q @ '>' | q @ '<' => { - let mut span = self.toks.next().unwrap().pos; - #[allow(clippy::eval_order_dependence)] - IntermediateValue::Op(if let Some(Token { kind: '=', .. }) = self.toks.peek() { - span = span.merge(self.toks.next().unwrap().pos); - match q { - '>' => Op::GreaterThanEqual, - '<' => Op::LessThanEqual, - _ => unreachable!(), - } - } else { - match q { - '>' => Op::GreaterThan, - '<' => Op::LessThan, - _ => unreachable!(), - } - }) - .span(span) - } - '=' => { - let mut span = self.toks.next().unwrap().pos(); - if let Some(Token { kind: '=', pos }) = self.toks.next() { - span = span.merge(pos); - IntermediateValue::Op(Op::Equal).span(span) - } else { - return Some(Err(("expected \"=\".", span).into())); - } - } - '!' => { - let mut span = self.toks.next().unwrap().pos(); - if let Some(Token { kind: '=', .. }) = self.toks.peek() { - span = span.merge(self.toks.next().unwrap().pos()); - return Some(Ok(IntermediateValue::Op(Op::NotEqual).span(span))); - } - self.whitespace(); - let v = match self.parse_identifier() { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - span = span.merge(v.span); - match v.node.to_ascii_lowercase().as_str() { - "important" => { - IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Important)) - .span(span) - } - _ => return Some(Err(("Expected \"important\".", span).into())), - } - } - '/' => { - let span = self.toks.next().unwrap().pos(); - match self.toks.peek() { - Some(Token { kind: '/', .. }) | Some(Token { kind: '*', .. }) => { - let span = match self.parse_comment() { - Ok(c) => c.span, - Err(e) => return Some(Err(e)), - }; - IntermediateValue::Whitespace.span(span) - } - Some(..) => IntermediateValue::Op(Op::Div).span(span), - None => return Some(Err(("Expected expression.", span).into())), - } - } - ';' | '}' | '{' => return None, - ':' | '?' | ')' | '@' | '^' | ']' | '|' => { - self.toks.next(); - return Some(Err(("expected \";\".", span).into())); - } - '\u{0}'..='\u{8}' | '\u{b}'..='\u{1f}' | '\u{7f}'..=std::char::MAX | '`' | '~' => { - self.toks.next(); - return Some(Err(("Expected expression.", span).into())); - } - ' ' | '\n' | '\t' => unreachable!("whitespace is checked prior to this match"), - 'A'..='Z' | 'a'..='z' | '_' | '\\' => { - unreachable!("these chars are checked in an if stmt") - } - })) - } - - fn parse_hex(&mut self) -> SassResult> { - let mut s = String::with_capacity(7); - s.push('#'); - let first_char = self - .toks - .peek() - .ok_or(("Expected identifier.", self.span_before))? - .kind; - let first_is_digit = first_char.is_ascii_digit(); - let first_is_hexdigit = first_char.is_ascii_hexdigit(); - if first_is_digit { - while let Some(c) = self.toks.peek() { - if !c.kind.is_ascii_hexdigit() || s.len() == 9 { - break; - } - let tok = self.toks.next().unwrap(); - self.span_before = self.span_before.merge(tok.pos()); - s.push(tok.kind); - } - // this branch exists so that we can emit `#` combined with - // identifiers. e.g. `#ooobar` should be emitted exactly as written; - // that is, `#ooobar`. - } else { - let ident = self.parse_identifier()?; - if first_is_hexdigit - && ident.node.chars().all(|c| c.is_ascii_hexdigit()) - && matches!(ident.node.len(), 3 | 4 | 6 | 8) - { - s.push_str(&ident.node); - } else { - return Ok(Spanned { - node: Value::String(format!("#{}", ident.node), QuoteKind::None), - span: ident.span, - }); - } - } - let v = match u32::from_str_radix(&s[1..], 16) { - Ok(a) => a, - Err(_) => return Ok(Value::String(s, QuoteKind::None).span(self.span_before)), - }; - let (red, green, blue, alpha) = match s.len().saturating_sub(1) { - 3 => ( - (((v & 0x0f00) >> 8) * 0x11) as u8, - (((v & 0x00f0) >> 4) * 0x11) as u8, - ((v & 0x000f) * 0x11) as u8, - 1, - ), - 4 => ( - (((v & 0xf000) >> 12) * 0x11) as u8, - (((v & 0x0f00) >> 8) * 0x11) as u8, - (((v & 0x00f0) >> 4) * 0x11) as u8, - ((v & 0x000f) * 0x11) as u8, - ), - 6 => ( - ((v & 0x00ff_0000) >> 16) as u8, - ((v & 0x0000_ff00) >> 8) as u8, - (v & 0x0000_00ff) as u8, - 1, - ), - 8 => ( - ((v & 0xff00_0000) >> 24) as u8, - ((v & 0x00ff_0000) >> 16) as u8, - ((v & 0x0000_ff00) >> 8) as u8, - (v & 0x0000_00ff) as u8, - ), - _ => return Err(("Expected hex digit.", self.span_before).into()), - }; - let color = Color::new(red, green, blue, alpha, s); - Ok(Value::Color(Box::new(color)).span(self.span_before)) - } -} - -struct IntermediateValueIterator<'a, 'b: 'a, 'c> { - parser: &'a mut Parser<'b, 'c>, - peek: Option>>, - predicate: Predicate<'a>, -} - -impl<'a, 'b: 'a, 'c> Iterator for IntermediateValueIterator<'a, 'b, 'c> { - type Item = SassResult>; - fn next(&mut self) -> Option { - if self.peek.is_some() { - self.peek.take() - } else { - self.parser.parse_intermediate_value(self.predicate) - } - } -} - -impl<'a, 'b: 'a, 'c> IntermediateValueIterator<'a, 'b, 'c> { - pub fn new(parser: &'a mut Parser<'b, 'c>, predicate: Predicate<'a>) -> Self { - Self { - parser, - peek: None, - predicate, - } - } - - fn peek(&mut self) -> &Option>> { - self.peek = self.next(); - &self.peek - } - - fn whitespace(&mut self) -> bool { - let mut found_whitespace = false; - while let Some(w) = self.peek() { - if !w.is_whitespace() { - break; - } - found_whitespace = true; - self.next(); - } - found_whitespace - } - - fn parse_op( - &mut self, - op: Spanned, - space_separated: &mut Vec>, - last_was_whitespace: bool, - in_paren: bool, - ) -> SassResult<()> { - match op.node { - Op::Not => { - self.whitespace(); - let right = self.single_value(in_paren)?; - space_separated.push(Spanned { - node: HigherIntermediateValue::UnaryOp(op.node, Box::new(right.node)), - span: right.span, - }); - } - Op::Div => { - self.whitespace(); - let right = self.single_value(in_paren)?; - if let Some(left) = space_separated.pop() { - space_separated.push(Spanned { - node: HigherIntermediateValue::BinaryOp( - Box::new(left.node), - op.node, - Box::new(right.node), - ), - span: left.span.merge(right.span), - }); - } else { - self.whitespace(); - space_separated.push(Spanned { - node: HigherIntermediateValue::Literal(Value::String( - format!( - "/{}", - ValueVisitor::new(self.parser, right.span) - .eval(right.node, false)? - .to_css_string( - right.span, - self.parser.options.is_compressed() - )? - ), - QuoteKind::None, - )), - span: op.span.merge(right.span), - }); - } - } - Op::Plus => { - self.whitespace(); - let right = self.single_value(in_paren)?; - - if let Some(left) = space_separated.pop() { - space_separated.push(Spanned { - node: HigherIntermediateValue::BinaryOp( - Box::new(left.node), - op.node, - Box::new(right.node), - ), - span: left.span.merge(right.span), - }); - } else { - space_separated.push(Spanned { - node: HigherIntermediateValue::UnaryOp(op.node, Box::new(right.node)), - span: right.span, - }); - } - } - Op::Minus => { - let may_be_subtraction = self.whitespace() || !last_was_whitespace; - let right = self.single_value(in_paren)?; - - if may_be_subtraction { - if let Some(left) = space_separated.pop() { - space_separated.push(Spanned { - node: HigherIntermediateValue::BinaryOp( - Box::new(left.node), - op.node, - Box::new(right.node), - ), - span: left.span.merge(right.span), - }); - } else { - space_separated.push( - right.map_node(|n| { - HigherIntermediateValue::UnaryOp(op.node, Box::new(n)) - }), - ); - } - } else { - space_separated.push( - right.map_node(|n| HigherIntermediateValue::UnaryOp(op.node, Box::new(n))), - ); - } - } - Op::And => { - self.whitespace(); - // special case when the value is literally "and" - if self.peek().is_none() { - space_separated.push( - HigherIntermediateValue::Literal(Value::String( - op.to_string(), - QuoteKind::None, - )) - .span(op.span), - ); - } else if let Some(left) = space_separated.pop() { - self.whitespace(); - if ValueVisitor::new(self.parser, left.span) - .eval(left.node.clone(), false)? - .is_true() - { - let right = self.single_value(in_paren)?; - space_separated.push( - HigherIntermediateValue::BinaryOp( - Box::new(left.node), - op.node, - Box::new(right.node), - ) - .span(left.span.merge(right.span)), - ); - } else { - // we explicitly ignore errors here as a workaround for short circuiting - while let Some(value) = self.peek() { - if let Ok(Spanned { - node: IntermediateValue::Comma, - .. - }) = value - { - break; - } - self.next(); - } - space_separated.push(left); - } - } else { - return Err(("Expected expression.", op.span).into()); - } - } - Op::Or => { - self.whitespace(); - // special case when the value is literally "or" - if self.peek().is_none() { - space_separated.push( - HigherIntermediateValue::Literal(Value::String( - op.to_string(), - QuoteKind::None, - )) - .span(op.span), - ); - } else if let Some(left) = space_separated.pop() { - self.whitespace(); - if ValueVisitor::new(self.parser, left.span) - .eval(left.node.clone(), false)? - .is_true() - { - // we explicitly ignore errors here as a workaround for short circuiting - while let Some(value) = self.peek() { - match value { - Ok(Spanned { - node: IntermediateValue::Comma, - .. - }) => break, - Ok(..) => { - self.next(); - } - Err(..) => { - if let Some(v) = self.next() { - v?; - } - } - } - } - space_separated.push(left); - } else { - let right = self.single_value(in_paren)?; - space_separated.push( - HigherIntermediateValue::BinaryOp( - Box::new(left.node), - op.node, - Box::new(right.node), - ) - .span(left.span.merge(right.span)), - ); - } - } else { - return Err(("Expected expression.", op.span).into()); - } - } - _ => { - if let Some(left) = space_separated.pop() { - self.whitespace(); - let right = self.single_value(in_paren)?; - space_separated.push( - HigherIntermediateValue::BinaryOp( - Box::new(left.node), - op.node, - Box::new(right.node), - ) - .span(left.span.merge(right.span)), - ); - } else { - return Err(("Expected expression.", op.span).into()); - } - } - } - Ok(()) - } - - #[allow(clippy::only_used_in_recursion)] - fn single_value(&mut self, in_paren: bool) -> SassResult> { - let next = self - .next() - .ok_or(("Expected expression.", self.parser.span_before))??; - Ok(match next.node { - IntermediateValue::Value(v) => v.span(next.span), - IntermediateValue::Op(op) => match op { - Op::Minus => { - self.whitespace(); - let val = self.single_value(in_paren)?; - Spanned { - node: HigherIntermediateValue::UnaryOp(Op::Minus, Box::new(val.node)), - span: next.span.merge(val.span), - } - } - Op::Not => { - self.whitespace(); - let val = self.single_value(in_paren)?; - Spanned { - node: HigherIntermediateValue::UnaryOp(Op::Not, Box::new(val.node)), - span: next.span.merge(val.span), - } - } - Op::Plus => { - self.whitespace(); - self.single_value(in_paren)? - } - Op::Div => { - self.whitespace(); - let val = self.single_value(in_paren)?; - Spanned { - node: HigherIntermediateValue::Literal(Value::String( - format!( - "/{}", - ValueVisitor::new(self.parser, val.span) - .eval(val.node, false)? - .to_css_string(val.span, self.parser.options.is_compressed())? - ), - QuoteKind::None, - )), - span: next.span.merge(val.span), - } - } - Op::And => Spanned { - node: HigherIntermediateValue::Literal(Value::String( - "and".into(), - QuoteKind::None, - )), - span: next.span, - }, - Op::Or => Spanned { - node: HigherIntermediateValue::Literal(Value::String( - "or".into(), - QuoteKind::None, - )), - span: next.span, - }, - _ => { - return Err(("Expected expression.", next.span).into()); - } - }, - IntermediateValue::Whitespace => unreachable!(), - IntermediateValue::Comma => { - return Err(("Expected expression.", self.parser.span_before).into()) - } - }) - } -} - -impl IsWhitespace for SassResult> { - fn is_whitespace(&self) -> bool { - match self { - Ok(v) => v.node.is_whitespace(), - _ => false, - } - } -} - -fn parse_i64(s: &str) -> i64 { - s.as_bytes() - .iter() - .fold(0, |total, this| total * 10 + i64::from(this - b'0')) -} - -fn is_keyword_operator(s: &str) -> bool { - matches!(s, "and" | "or" | "not") -} diff --git a/src/parse/variable.rs b/src/parse/variable.rs deleted file mode 100644 index 9aa10215..00000000 --- a/src/parse/variable.rs +++ /dev/null @@ -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>, - pub global: bool, - pub default: bool, -} - -impl VariableValue { - pub const fn new(var_value: SassResult>, 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 { - 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)) - } -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index bcfe8bc3..00000000 --- a/src/scope.rs +++ /dev/null @@ -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, - pub mixins: BTreeMap, - pub functions: BTreeMap, -} - -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) -> 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 { - self.vars.insert(s, v) - } - - pub fn var_exists(&self, name: Identifier) -> bool { - self.vars.contains_key(&name) - } - - fn get_mixin(&self, name: Spanned) -> SassResult { - match self.mixins.get(&name.node) { - Some(v) => Ok(v.clone()), - None => Err(("Undefined mixin.", name.span).into()), - } - } - - pub fn insert_mixin>(&mut self, s: T, v: Mixin) -> Option { - 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 { - self.functions.get(&name).cloned() - } - - pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option { - 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); - -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 { - 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 { - 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, - 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 { - 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, - global_scope: &'a Scope, - ) -> SassResult { - 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 { - 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 { - 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()) - } -} diff --git a/src/selector/attribute.rs b/src/selector/attribute.rs index 31345632..32e657f6 100644 --- a/src/selector/attribute.rs +++ b/src/selector/attribute.rs @@ -6,10 +6,10 @@ use std::{ use codemap::Span; use crate::{ - common::QuoteKind, error::SassResult, parse::Parser, utils::is_ident, value::Value, Token, + common::QuoteKind, error::SassResult, parse::BaseParser, utils::is_ident, value::Value, Token, }; -use super::{Namespace, QualifiedName}; +use super::{Namespace, QualifiedName, SelectorParser}; #[derive(Clone, Debug)] pub(crate) struct Attribute { @@ -40,52 +40,53 @@ impl Hash for Attribute { } } -fn attribute_name(parser: &mut Parser, start: Span) -> SassResult { - let next = parser.toks.peek().ok_or(("Expected identifier.", start))?; +// todo: rewrite +fn attribute_name(parser: &mut SelectorParser) -> SassResult { + let next = parser + .toks + .peek() + .ok_or(("Expected identifier.", parser.toks.current_span()))?; if next.kind == '*' { parser.toks.next(); parser.expect_char('|')?; - let ident = parser.parse_identifier()?.node; + let ident = parser.parse_identifier(false, false)?; return Ok(QualifiedName { ident, namespace: Namespace::Asterisk, }); } - parser.span_before = next.pos; - let name_or_namespace = parser.parse_identifier()?; + + let name_or_namespace = parser.parse_identifier(false, false)?; match parser.toks.peek() { Some(v) if v.kind != '|' => { return Ok(QualifiedName { - ident: name_or_namespace.node, + ident: name_or_namespace, namespace: Namespace::None, }); } Some(..) => {} - None => return Err(("expected more input.", name_or_namespace.span).into()), + None => return Err(("expected more input.", parser.toks.current_span()).into()), } - match parser.toks.peek_forward(1) { + match parser.toks.peek_n(1) { Some(v) if v.kind == '=' => { - parser.toks.reset_cursor(); return Ok(QualifiedName { - ident: name_or_namespace.node, + ident: name_or_namespace, namespace: Namespace::None, }); } - Some(..) => { - parser.toks.reset_cursor(); - } - None => return Err(("expected more input.", name_or_namespace.span).into()), + Some(..) => {} + None => return Err(("expected more input.", parser.toks.current_span()).into()), } - parser.span_before = parser.toks.next().unwrap().pos(); - let ident = parser.parse_identifier()?.node; + parser.toks.next(); + let ident = parser.parse_identifier(false, false)?; Ok(QualifiedName { ident, - namespace: Namespace::Other(name_or_namespace.node.into_boxed_str()), + namespace: Namespace::Other(name_or_namespace.into_boxed_str()), }) } -fn attribute_operator(parser: &mut Parser) -> SassResult { +fn attribute_operator(parser: &mut SelectorParser) -> SassResult { let op = match parser.toks.next() { Some(Token { kind: '=', .. }) => return Ok(AttributeOp::Equals), Some(Token { kind: '~', .. }) => AttributeOp::Include, @@ -93,7 +94,7 @@ fn attribute_operator(parser: &mut Parser) -> SassResult { Some(Token { kind: '^', .. }) => AttributeOp::Prefix, Some(Token { kind: '$', .. }) => AttributeOp::Suffix, Some(Token { kind: '*', .. }) => AttributeOp::Contains, - Some(..) | None => return Err(("Expected \"]\".", parser.span_before).into()), + Some(..) | None => return Err(("Expected \"]\".", parser.toks.current_span()).into()), }; parser.expect_char('=')?; @@ -101,15 +102,15 @@ fn attribute_operator(parser: &mut Parser) -> SassResult { Ok(op) } impl Attribute { - pub fn from_tokens(parser: &mut Parser) -> SassResult { - let start = parser.span_before; - parser.whitespace(); - let attr = attribute_name(parser, start)?; - parser.whitespace(); + pub fn from_tokens(parser: &mut SelectorParser) -> SassResult { + let start = parser.toks.cursor(); + parser.whitespace_without_comments(); + let attr = attribute_name(parser)?; + parser.whitespace_without_comments(); if parser .toks .peek() - .ok_or(("expected more input.", start))? + .ok_or(("expected more input.", parser.toks.current_span()))? .kind == ']' { @@ -119,27 +120,23 @@ impl Attribute { value: String::new(), modifier: None, op: AttributeOp::Any, - span: start, + span: parser.toks.span_from(start), }); } - parser.span_before = start; let op = attribute_operator(parser)?; - parser.whitespace(); + parser.whitespace_without_comments(); + + let peek = parser + .toks + .peek() + .ok_or(("expected more input.", parser.toks.current_span()))?; - let peek = parser.toks.peek().ok_or(("expected more input.", start))?; - parser.span_before = peek.pos; let value = match peek.kind { - q @ '\'' | q @ '"' => { - parser.toks.next(); - match parser.parse_quoted_string(q)?.node { - Value::String(s, ..) => s, - _ => unreachable!(), - } - } - _ => parser.parse_identifier()?.node, + '\'' | '"' => parser.parse_string()?, + _ => parser.parse_identifier(false, false)?, }; - parser.whitespace(); + parser.whitespace_without_comments(); let modifier = match parser.toks.peek() { Some(Token { @@ -151,7 +148,7 @@ impl Attribute { .. }) => { parser.toks.next(); - parser.whitespace(); + parser.whitespace_without_comments(); Some(c) } _ => None, @@ -164,7 +161,7 @@ impl Attribute { attr, value, modifier, - span: start, + span: parser.toks.span_from(start), }) } } diff --git a/src/selector/extend/extended_selector.rs b/src/selector/extend/extended_selector.rs index 86fb84e0..0c4189d4 100644 --- a/src/selector/extend/extended_selector.rs +++ b/src/selector/extend/extended_selector.rs @@ -37,6 +37,10 @@ impl ExtendedSelector { Self(Rc::new(RefCell::new(selector))) } + pub fn is_invisible(&self) -> bool { + (*self.0).borrow().is_invisible() + } + pub fn into_selector(self) -> Selector { Selector(match Rc::try_unwrap(self.0) { Ok(v) => v.into_inner(), diff --git a/src/selector/extend/extension.rs b/src/selector/extend/extension.rs index f766027e..60821d3f 100644 --- a/src/selector/extend/extension.rs +++ b/src/selector/extend/extension.rs @@ -1,6 +1,8 @@ use codemap::Span; -use super::{ComplexSelector, CssMediaQuery, SimpleSelector}; +use crate::ast::CssMediaQuery; + +use super::{ComplexSelector, SimpleSelector}; #[derive(Clone, Debug)] pub(crate) struct Extension { diff --git a/src/selector/extend/mod.rs b/src/selector/extend/mod.rs index 4eeaffee..38a5bace 100644 --- a/src/selector/extend/mod.rs +++ b/src/selector/extend/mod.rs @@ -7,7 +7,7 @@ use codemap::Span; use indexmap::IndexMap; -use crate::error::SassResult; +use crate::{ast::CssMediaQuery, error::SassResult}; use super::{ ComplexSelector, ComplexSelectorComponent, ComplexSelectorHashSet, CompoundSelector, Pseudo, @@ -28,9 +28,6 @@ mod functions; mod merged; mod rule; -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub(crate) struct CssMediaQuery; - #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] /// Different modes in which extension can run. enum ExtendMode { @@ -59,7 +56,7 @@ impl Default for ExtendMode { } #[derive(Clone, Debug)] -pub(crate) struct Extender { +pub(crate) struct ExtensionStore { /// A map from all simple selectors in the stylesheet to the selector lists /// that contain them. /// @@ -107,7 +104,7 @@ pub(crate) struct Extender { span: Span, } -impl Extender { +impl ExtensionStore { /// An `Extender` that contains no extensions and can have no extensions added. // TODO: empty extender #[allow(dead_code)] @@ -185,7 +182,7 @@ impl Extender { }) .collect(); - let mut extender = Extender::with_mode(mode, span); + let mut extender = ExtensionStore::with_mode(mode, span); if !selector.is_invisible() { extender.originals.extend(selector.components.iter()); @@ -197,7 +194,7 @@ impl Extender { fn with_mode(mode: ExtendMode, span: Span) -> Self { Self { mode, - ..Extender::new(span) + ..ExtensionStore::new(span) } } @@ -646,13 +643,14 @@ impl Extender { // supporting it properly would make this code and the code calling it // a lot more complicated, so it's not supported for now. let inner_pseudo_normalized = inner_pseudo.normalized_name(); - if inner_pseudo_normalized == "matches" || inner_pseudo_normalized == "is" { + if ["matches", "is", "where"].contains(&inner_pseudo_normalized) { inner_pseudo.selector.clone().unwrap().components } else { Vec::new() } } - "matches" | "is" | "any" | "current" | "nth-child" | "nth-last-child" => { + "matches" | "where" | "is" | "any" | "current" | "nth-child" + | "nth-last-child" => { // As above, we could theoretically support :not within :matches, but // doing so would require this method and its callers to handle much // more complex cases that likely aren't worth the pain. @@ -721,6 +719,7 @@ impl Extender { } let mut tmp = vec![self.extension_for_simple(simple)]; + tmp.reserve(extenders.len()); tmp.extend(extenders.values().cloned()); Some(tmp) @@ -865,7 +864,7 @@ impl Extender { &mut self, mut selector: SelectorList, // span: Span, - media_query_context: Option>, + media_query_context: &Option>, ) -> ExtendedSelector { if !selector.is_invisible() { for complex in selector.components.clone() { @@ -874,7 +873,7 @@ impl Extender { } if !self.extensions.is_empty() { - selector = self.extend_list(selector, None, &media_query_context); + selector = self.extend_list(selector, None, media_query_context); /* todo: when we have error handling } on SassException catch (error) { @@ -885,7 +884,7 @@ impl Extender { } */ } - if let Some(mut media_query_context) = media_query_context { + if let Some(mut media_query_context) = media_query_context.clone() { self.media_contexts .get_mut(&selector) .replace(&mut media_query_context); diff --git a/src/selector/extend/rule.rs b/src/selector/extend/rule.rs index 25da0eba..52199ed7 100644 --- a/src/selector/extend/rule.rs +++ b/src/selector/extend/rule.rs @@ -1,22 +1,4 @@ -use codemap::Span; - -use crate::selector::Selector; - #[derive(Clone, Debug)] pub(crate) struct ExtendRule { - #[allow(dead_code)] - pub selector: Selector, pub is_optional: bool, - #[allow(dead_code)] - pub span: Span, -} - -impl ExtendRule { - pub const fn new(selector: Selector, is_optional: bool, span: Span) -> Self { - Self { - selector, - is_optional, - span, - } - } } diff --git a/src/selector/mod.rs b/src/selector/mod.rs index 21122e59..8040d889 100644 --- a/src/selector/mod.rs +++ b/src/selector/mod.rs @@ -1,5 +1,3 @@ -use std::fmt; - use codemap::Span; use crate::{error::SassResult, value::Value}; @@ -22,15 +20,10 @@ mod list; mod parse; mod simple; +// todo: delete this selector wrapper #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Selector(pub SelectorList); -impl fmt::Display for Selector { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - impl Selector { /// Small wrapper around `SelectorList`'s method that turns an empty parent selector /// into `None`. This is a hack and in the future should be replaced. @@ -58,18 +51,6 @@ impl Selector { self.0.contains_parent_selector() } - pub fn remove_placeholders(self) -> Selector { - Self(SelectorList { - span: self.0.span, - components: self - .0 - .components - .into_iter() - .filter(|c| !c.is_invisible()) - .collect(), - }) - } - pub fn is_empty(&self) -> bool { self.0.is_empty() } diff --git a/src/selector/parse.rs b/src/selector/parse.rs index 5246f065..10d1e04b 100644 --- a/src/selector/parse.rs +++ b/src/selector/parse.rs @@ -1,6 +1,6 @@ use codemap::Span; -use crate::{common::unvendor, error::SassResult, parse::Parser, utils::is_name, Token}; +use crate::{common::unvendor, error::SassResult, lexer::Lexer, parse::BaseParser, Token}; use super::{ Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, @@ -17,22 +17,11 @@ enum DevouredWhitespace { None, } -impl DevouredWhitespace { - fn found_whitespace(&mut self) { - if self == &Self::None { - *self = Self::Whitespace; - } - } - - fn found_newline(&mut self) { - *self = Self::Newline; - } -} - /// Pseudo-class selectors that take unadorned selectors as arguments. -const SELECTOR_PSEUDO_CLASSES: [&str; 8] = [ +const SELECTOR_PSEUDO_CLASSES: [&str; 9] = [ "not", "matches", + "where", "is", "current", "any", @@ -44,36 +33,41 @@ const SELECTOR_PSEUDO_CLASSES: [&str; 8] = [ /// Pseudo-element selectors that take unadorned selectors as arguments. const SELECTOR_PSEUDO_ELEMENTS: [&str; 1] = ["slotted"]; -pub(crate) struct SelectorParser<'a, 'b, 'c> { +pub(crate) struct SelectorParser<'a> { /// Whether this parser allows the parent selector `&`. allows_parent: bool, /// Whether this parser allows placeholder selectors beginning with `%`. allows_placeholder: bool, - parser: &'a mut Parser<'b, 'c>, + pub toks: Lexer<'a>, span: Span, } -impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { - pub fn new( - parser: &'a mut Parser<'b, 'c>, - allows_parent: bool, - allows_placeholder: bool, - span: Span, - ) -> Self { +impl<'a> BaseParser<'a> for SelectorParser<'a> { + fn toks(&self) -> &Lexer<'a> { + &self.toks + } + + fn toks_mut(&mut self) -> &mut Lexer<'a> { + &mut self.toks + } +} + +impl<'a> SelectorParser<'a> { + pub fn new(toks: Lexer<'a>, allows_parent: bool, allows_placeholder: bool, span: Span) -> Self { Self { + toks, allows_parent, allows_placeholder, - parser, span, } } pub fn parse(mut self) -> SassResult { let tmp = self.parse_selector_list()?; - if self.parser.toks.peek().is_some() { + if self.toks.peek().is_some() { return Err(("expected selector.", self.span).into()); } Ok(tmp) @@ -82,14 +76,13 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { fn parse_selector_list(&mut self) -> SassResult { let mut components = vec![self.parse_complex_selector(false)?]; - self.parser.whitespace(); + self.whitespace()?; let mut line_break = false; - while let Some(Token { kind: ',', .. }) = self.parser.toks.peek() { - self.parser.toks.next(); + while self.scan_char(',') { line_break = self.eat_whitespace() == DevouredWhitespace::Newline || line_break; - match self.parser.toks.peek() { + match self.toks.peek() { Some(Token { kind: ',', .. }) => continue, Some(..) => {} None => break, @@ -106,17 +99,15 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { } fn eat_whitespace(&mut self) -> DevouredWhitespace { - let mut whitespace_devoured = DevouredWhitespace::None; - while let Some(tok) = self.parser.toks.peek() { - match tok.kind { - ' ' | '\t' => whitespace_devoured.found_whitespace(), - '\n' => whitespace_devoured.found_newline(), - _ => break, - } - self.parser.toks.next(); - } + let text = self.raw_text(Self::whitespace); - whitespace_devoured + if text.contains('\n') { + DevouredWhitespace::Newline + } else if !text.is_empty() { + DevouredWhitespace::Whitespace + } else { + DevouredWhitespace::None + } } /// Consumes a complex selector. @@ -127,22 +118,22 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { let mut components = Vec::new(); loop { - self.parser.whitespace(); + self.whitespace()?; - // todo: can we do while let Some(..) = self.parser.toks.peek() ? - match self.parser.toks.peek() { + // todo: can we do while let Some(..) = self.toks.peek() ? + match self.toks.peek() { Some(Token { kind: '+', .. }) => { - self.parser.toks.next(); + self.toks.next(); components.push(ComplexSelectorComponent::Combinator( Combinator::NextSibling, )); } Some(Token { kind: '>', .. }) => { - self.parser.toks.next(); + self.toks.next(); components.push(ComplexSelectorComponent::Combinator(Combinator::Child)); } Some(Token { kind: '~', .. }) => { - self.parser.toks.next(); + self.toks.next(); components.push(ComplexSelectorComponent::Combinator( Combinator::FollowingSibling, )); @@ -159,18 +150,18 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { components.push(ComplexSelectorComponent::Compound( self.parse_compound_selector()?, )); - if let Some(Token { kind: '&', .. }) = self.parser.toks.peek() { + if let Some(Token { kind: '&', .. }) = self.toks.peek() { return Err(("\"&\" may only used at the beginning of a compound selector.", self.span).into()); } } Some(..) => { - if !self.parser.looking_at_identifier() { + if !self.looking_at_identifier() { break; } components.push(ComplexSelectorComponent::Compound( self.parse_compound_selector()?, )); - if let Some(Token { kind: '&', .. }) = self.parser.toks.peek() { + if let Some(Token { kind: '&', .. }) = self.toks.peek() { return Err(("\"&\" may only used at the beginning of a compound selector.", self.span).into()); } } @@ -188,7 +179,7 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { fn parse_compound_selector(&mut self) -> SassResult { let mut components = vec![self.parse_simple_selector(None)?]; - while let Some(Token { kind, .. }) = self.parser.toks.peek() { + while let Some(Token { kind, .. }) = self.toks.peek() { if !is_simple_selector_start(kind) { break; } @@ -199,16 +190,12 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { Ok(CompoundSelector { components }) } - fn looking_at_identifier_body(&mut self) -> bool { - matches!(self.parser.toks.peek(), Some(t) if is_name(t.kind) || t.kind == '\\') - } - /// Consumes a simple selector. /// /// If `allows_parent` is `Some`, this will override `self.allows_parent`. If `allows_parent` /// is `None`, it will fallback to `self.allows_parent`. fn parse_simple_selector(&mut self, allows_parent: Option) -> SassResult { - match self.parser.toks.peek() { + match self.toks.peek() { Some(Token { kind: '[', .. }) => self.parse_attribute_selector(), Some(Token { kind: '.', .. }) => self.parse_class_selector(), Some(Token { kind: '#', .. }) => self.parse_id_selector(), @@ -232,40 +219,33 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { } fn parse_attribute_selector(&mut self) -> SassResult { - self.parser.toks.next(); + self.toks.next(); Ok(SimpleSelector::Attribute(Box::new(Attribute::from_tokens( - self.parser, + self, )?))) } fn parse_class_selector(&mut self) -> SassResult { - self.parser.toks.next(); - Ok(SimpleSelector::Class(self.parser.parse_identifier()?.node)) + self.toks.next(); + Ok(SimpleSelector::Class(self.parse_identifier(false, false)?)) } fn parse_id_selector(&mut self) -> SassResult { - self.parser.toks.next(); - Ok(SimpleSelector::Id(self.parser.parse_identifier()?.node)) + self.toks.next(); + Ok(SimpleSelector::Id(self.parse_identifier(false, false)?)) } fn parse_pseudo_selector(&mut self) -> SassResult { - self.parser.toks.next(); - let element = match self.parser.toks.peek() { - Some(Token { kind: ':', .. }) => { - self.parser.toks.next(); - true - } - _ => false, - }; + self.toks.next(); + let element = self.scan_char(':'); + let name = self.parse_identifier(false, false)?; - let name = self.parser.parse_identifier()?; - - match self.parser.toks.peek() { - Some(Token { kind: '(', .. }) => self.parser.toks.next(), + match self.toks.peek() { + Some(Token { kind: '(', .. }) => self.toks.next(), _ => { return Ok(SimpleSelector::Pseudo(Pseudo { is_class: !element && !is_fake_pseudo_element(&name), - name: name.node, + name, selector: None, is_syntactic_class: !element, argument: None, @@ -274,7 +254,7 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { } }; - self.parser.whitespace(); + self.whitespace()?; let unvendored = unvendor(&name); @@ -285,51 +265,50 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { // todo: lowercase? if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); - self.parser.whitespace(); + self.whitespace()?; } else { - argument = Some( - self.parser - .declaration_value(true, false, true)? - .into_boxed_str(), - ); + argument = Some(self.declaration_value(true)?.into_boxed_str()); } - self.parser.expect_char(')')?; + self.expect_char(')')?; } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); - self.parser.whitespace(); - self.parser.expect_char(')')?; + self.whitespace()?; + self.expect_char(')')?; } else if unvendored == "nth-child" || unvendored == "nth-last-child" { let mut this_arg = self.parse_a_n_plus_b()?; - let found_whitespace = self.parser.whitespace(); - #[allow(clippy::match_same_arms)] - match (found_whitespace, self.parser.toks.peek()) { - (_, Some(Token { kind: ')', .. })) => {} - (true, _) => { - self.expect_identifier("of")?; - this_arg.push_str(" of"); - self.parser.whitespace(); - selector = Some(Box::new(self.parse_selector_list()?)); - } - _ => {} + self.whitespace()?; + + let last_was_whitespace = matches!( + self.toks.peek_n_backwards(1), + Some(Token { + kind: ' ' | '\t' | '\n' | '\r', + .. + }) + ); + if last_was_whitespace && !matches!(self.toks.peek(), Some(Token { kind: ')', .. })) { + self.expect_identifier("of", false)?; + this_arg.push_str(" of"); + self.whitespace()?; + selector = Some(Box::new(self.parse_selector_list()?)); } - self.parser.expect_char(')')?; + + self.expect_char(')')?; argument = Some(this_arg.into_boxed_str()); } else { argument = Some( - self.parser - .declaration_value(true, false, true)? + self.declaration_value(true)? .trim_end() .to_owned() .into_boxed_str(), ); - self.parser.expect_char(')')?; + self.expect_char(')')?; } Ok(SimpleSelector::Pseudo(Pseudo { is_class: !element && !is_fake_pseudo_element(&name), - name: name.node, + name, selector, is_syntactic_class: !element, argument, @@ -338,9 +317,11 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { } fn parse_parent_selector(&mut self) -> SassResult { - self.parser.toks.next(); + self.toks.next(); let suffix = if self.looking_at_identifier_body() { - Some(self.parser.parse_identifier()?.node) + let mut buffer = String::new(); + self.parse_identifier_body(&mut buffer, false, false)?; + Some(buffer) } else { None }; @@ -348,9 +329,9 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { } fn parse_placeholder_selector(&mut self) -> SassResult { - self.parser.toks.next(); + self.toks.next(); Ok(SimpleSelector::Placeholder( - self.parser.parse_identifier()?.node, + self.parse_identifier(false, false)?, )) } @@ -358,38 +339,34 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { /// /// These are combined because either one could start with `*`. fn parse_type_or_universal_selector(&mut self) -> SassResult { - self.parser.toks.peek(); - - match self.parser.toks.peek() { - Some(Token { kind: '*', pos }) => { - self.parser.span_before = self.parser.span_before.merge(pos); - self.parser.toks.next(); - if let Some(Token { kind: '|', .. }) = self.parser.toks.peek() { - self.parser.toks.next(); - if let Some(Token { kind: '*', .. }) = self.parser.toks.peek() { - self.parser.toks.next(); + match self.toks.peek() { + Some(Token { kind: '*', .. }) => { + self.toks.next(); + if let Some(Token { kind: '|', .. }) = self.toks.peek() { + self.toks.next(); + if let Some(Token { kind: '*', .. }) = self.toks.peek() { + self.toks.next(); return Ok(SimpleSelector::Universal(Namespace::Asterisk)); } return Ok(SimpleSelector::Type(QualifiedName { - ident: self.parser.parse_identifier()?.node, + ident: self.parse_identifier(false, false)?, namespace: Namespace::Asterisk, })); } return Ok(SimpleSelector::Universal(Namespace::None)); } - Some(Token { kind: '|', pos }) => { - self.parser.span_before = self.parser.span_before.merge(pos); - self.parser.toks.next(); - match self.parser.toks.peek() { + Some(Token { kind: '|', .. }) => { + self.toks.next(); + match self.toks.peek() { Some(Token { kind: '*', .. }) => { - self.parser.toks.next(); + self.toks.next(); return Ok(SimpleSelector::Universal(Namespace::Empty)); } _ => { return Ok(SimpleSelector::Type(QualifiedName { - ident: self.parser.parse_identifier()?.node, + ident: self.parse_identifier(false, false)?, namespace: Namespace::Empty, })); } @@ -398,17 +375,17 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { _ => {} } - let name_or_namespace = self.parser.parse_identifier()?.node; + let name_or_namespace = self.parse_identifier(false, false)?; - Ok(match self.parser.toks.peek() { + Ok(match self.toks.peek() { Some(Token { kind: '|', .. }) => { - self.parser.toks.next(); - if let Some(Token { kind: '*', .. }) = self.parser.toks.peek() { - self.parser.toks.next(); + self.toks.next(); + if let Some(Token { kind: '*', .. }) = self.toks.peek() { + self.toks.next(); SimpleSelector::Universal(Namespace::Other(name_or_namespace.into_boxed_str())) } else { SimpleSelector::Type(QualifiedName { - ident: self.parser.parse_identifier()?.node, + ident: self.parse_identifier(false, false)?, namespace: Namespace::Other(name_or_namespace.into_boxed_str()), }) } @@ -426,60 +403,51 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { fn parse_a_n_plus_b(&mut self) -> SassResult { let mut buf = String::new(); - match self.parser.toks.peek() { + match self.toks.peek() { Some(Token { kind: 'e', .. }) | Some(Token { kind: 'E', .. }) => { - self.expect_identifier("even")?; + self.expect_identifier("even", false)?; return Ok("even".to_owned()); } Some(Token { kind: 'o', .. }) | Some(Token { kind: 'O', .. }) => { - self.expect_identifier("odd")?; + self.expect_identifier("odd", false)?; return Ok("odd".to_owned()); } Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) => { buf.push(t.kind); - self.parser.toks.next(); + self.toks.next(); } _ => {} } - match self.parser.toks.peek() { + match self.toks.peek() { Some(t) if t.kind.is_ascii_digit() => { - while let Some(t) = self.parser.toks.peek() { + while let Some(t) = self.toks.peek() { if !t.kind.is_ascii_digit() { break; } buf.push(t.kind); - self.parser.toks.next(); + self.toks.next(); } - self.parser.whitespace(); - if let Some(t) = self.parser.toks.peek() { - if t.kind != 'n' && t.kind != 'N' { - return Ok(buf); - } - self.parser.toks.next(); - } - } - Some(t) => { - if t.kind == 'n' || t.kind == 'N' { - self.parser.toks.next(); - } else { - return Err(("Expected \"n\".", self.span).into()); + self.whitespace()?; + if !self.scan_ident_char('n', false)? { + return Ok(buf); } } + Some(..) => self.expect_ident_char('n', false)?, None => return Err(("expected more input.", self.span).into()), } buf.push('n'); - self.parser.whitespace(); + self.whitespace()?; if let Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) = - self.parser.toks.peek() + self.toks.peek() { buf.push(t.kind); - self.parser.toks.next(); - self.parser.whitespace(); - match self.parser.toks.peek() { + self.toks.next(); + self.whitespace()?; + match self.toks.peek() { Some(t) if !t.kind.is_ascii_digit() => { return Err(("Expected a number.", self.span).into()) } @@ -487,26 +455,16 @@ impl<'a, 'b, 'c> SelectorParser<'a, 'b, 'c> { Some(..) => {} } - while let Some(t) = self.parser.toks.peek() { + while let Some(t) = self.toks.peek() { if !t.kind.is_ascii_digit() { break; } buf.push(t.kind); - self.parser.toks.next(); + self.toks.next(); } } Ok(buf) } - - fn expect_identifier(&mut self, s: &str) -> SassResult<()> { - let mut ident = self.parser.parse_identifier_no_interpolation(false)?.node; - ident.make_ascii_lowercase(); - if ident == s { - Ok(()) - } else { - Err((format!("Expected \"{}\".", s), self.span).into()) - } - } } /// Returns whether `c` can start a simple selector other than a type diff --git a/src/selector/simple.rs b/src/selector/simple.rs index ebebe41a..5187f992 100644 --- a/src/selector/simple.rs +++ b/src/selector/simple.rs @@ -12,7 +12,14 @@ use super::{ QualifiedName, SelectorList, Specificity, }; -const SUBSELECTOR_PSEUDOS: [&str; 5] = ["matches", "is", "any", "nth-child", "nth-last-child"]; +const SUBSELECTOR_PSEUDOS: [&str; 6] = [ + "matches", + "where", + "is", + "any", + "nth-child", + "nth-last-child", +]; const BASE_SPECIFICITY: i32 = 1000; @@ -122,7 +129,7 @@ impl SimpleSelector { name != "not" && selector.as_ref().map_or(false, |sel| sel.is_invisible()) } Self::Placeholder(..) => true, - Self::Parent(..) => todo!(), + Self::Parent(..) => unreachable!("parent selectors should be resolved at this point"), } } @@ -487,7 +494,7 @@ impl Pseudo { ) -> bool { debug_assert!(self.selector.is_some()); match self.normalized_name() { - "matches" | "is" | "any" => { + "matches" | "is" | "any" | "where" => { selector_pseudos_named(compound.clone(), &self.name, true).any(move |pseudo2| { self.selector .as_ref() diff --git a/src/serializer.rs b/src/serializer.rs new file mode 100644 index 00000000..c038f78f --- /dev/null +++ b/src/serializer.rs @@ -0,0 +1,856 @@ +use std::io::Write; + +use codemap::{CodeMap, Span, Spanned}; + +use crate::{ + ast::{CssStmt, MediaQuery, Style, SupportsRule}, + color::{Color, ColorFormat, NAMED_COLORS}, + error::SassResult, + selector::{ + Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo, + SelectorList, SimpleSelector, + }, + utils::hex_char_for, + value::{fuzzy_equals, CalculationArg, SassCalculation, SassNumber, Value}, + Options, +}; + +pub(crate) fn serialize_color(color: &Color, options: &Options, span: Span) -> String { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, false, span); + + serializer.visit_color(color); + + serializer.finish_for_expr() +} + +pub(crate) fn serialize_selector_list( + list: &SelectorList, + options: &Options, + span: Span, +) -> String { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, false, span); + + serializer.write_selector_list(list); + + serializer.finish_for_expr() +} + +pub(crate) fn serialize_calculation( + calculation: &SassCalculation, + options: &Options, + span: Span, +) -> SassResult { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, false, span); + + serializer.visit_calculation(calculation)?; + + Ok(serializer.finish_for_expr()) +} + +pub(crate) fn serialize_calculation_arg( + arg: &CalculationArg, + options: &Options, + span: Span, +) -> SassResult { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, false, span); + + serializer.write_calculation_arg(arg)?; + + Ok(serializer.finish_for_expr()) +} + +pub(crate) fn serialize_number( + number: &SassNumber, + options: &Options, + span: Span, +) -> SassResult { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, false, span); + + serializer.visit_number(number)?; + + Ok(serializer.finish_for_expr()) +} + +pub(crate) fn inspect_number( + number: &SassNumber, + options: &Options, + span: Span, +) -> SassResult { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, true, span); + + serializer.visit_number(number)?; + + Ok(serializer.finish_for_expr()) +} + +pub(crate) struct Serializer<'a> { + indentation: usize, + options: &'a Options<'a>, + inspect: bool, + indent_width: usize, + // todo: use this field + _quote: bool, + buffer: Vec, + map: &'a CodeMap, + span: Span, +} + +impl<'a> Serializer<'a> { + pub fn new(options: &'a Options<'a>, map: &'a CodeMap, inspect: bool, span: Span) -> Self { + Self { + inspect, + _quote: true, + indentation: 0, + indent_width: 2, + options, + buffer: Vec::new(), + map, + span, + } + } + + fn omit_spaces_around_complex_component(&self, component: &ComplexSelectorComponent) -> bool { + self.options.is_compressed() + && matches!(component, ComplexSelectorComponent::Combinator(..)) + } + + fn write_pseudo_selector(&mut self, pseudo: &Pseudo) { + if let Some(sel) = &pseudo.selector { + if pseudo.name == "not" && sel.is_invisible() { + return; + } + } + + self.buffer.push(b':'); + + if !pseudo.is_syntactic_class { + self.buffer.push(b':'); + } + + self.buffer.extend_from_slice(pseudo.name.as_bytes()); + + if pseudo.argument.is_none() && pseudo.selector.is_none() { + return; + } + + self.buffer.push(b'('); + if let Some(arg) = &pseudo.argument { + self.buffer.extend_from_slice(arg.as_bytes()); + if pseudo.selector.is_some() { + self.buffer.push(b' '); + } + } + + if let Some(sel) = &pseudo.selector { + self.write_selector_list(sel); + } + + self.buffer.push(b')'); + } + + fn write_namespace(&mut self, namespace: &Namespace) { + match namespace { + Namespace::Empty => self.buffer.push(b'|'), + Namespace::Asterisk => self.buffer.extend_from_slice(b"*|"), + Namespace::Other(namespace) => { + self.buffer.extend_from_slice(namespace.as_bytes()); + self.buffer.push(b'|'); + } + Namespace::None => {} + } + } + + fn write_simple_selector(&mut self, simple: &SimpleSelector) { + match simple { + SimpleSelector::Id(name) => { + self.buffer.push(b'#'); + self.buffer.extend_from_slice(name.as_bytes()); + } + SimpleSelector::Class(name) => { + self.buffer.push(b'.'); + self.buffer.extend_from_slice(name.as_bytes()); + } + SimpleSelector::Placeholder(name) => { + self.buffer.push(b'%'); + self.buffer.extend_from_slice(name.as_bytes()); + } + SimpleSelector::Universal(namespace) => { + self.write_namespace(namespace); + self.buffer.push(b'*'); + } + SimpleSelector::Pseudo(pseudo) => self.write_pseudo_selector(pseudo), + SimpleSelector::Type(name) => { + self.write_namespace(&name.namespace); + self.buffer.extend_from_slice(name.ident.as_bytes()); + } + SimpleSelector::Attribute(attr) => write!(&mut self.buffer, "{}", attr).unwrap(), + SimpleSelector::Parent(..) => unreachable!("It should not be possible to format `&`."), + } + } + + fn write_compound_selector(&mut self, compound: &CompoundSelector) { + let mut did_write = false; + for simple in &compound.components { + if did_write { + self.write_simple_selector(simple); + } else { + let len = self.buffer.len(); + self.write_simple_selector(simple); + if self.buffer.len() != len { + did_write = true; + } + } + } + + // If we emit an empty compound, it's because all of the components got + // optimized out because they match all selectors, so we just emit the + // universal selector. + if !did_write { + self.buffer.push(b'*'); + } + } + + fn write_complex_selector_component(&mut self, component: &ComplexSelectorComponent) { + match component { + ComplexSelectorComponent::Combinator(Combinator::NextSibling) => self.buffer.push(b'+'), + ComplexSelectorComponent::Combinator(Combinator::Child) => self.buffer.push(b'>'), + ComplexSelectorComponent::Combinator(Combinator::FollowingSibling) => { + self.buffer.push(b'~') + } + ComplexSelectorComponent::Compound(compound) => self.write_compound_selector(compound), + } + } + + fn write_complex_selector(&mut self, complex: &ComplexSelector) { + let mut last_component = None; + + for component in &complex.components { + if let Some(c) = last_component { + if !self.omit_spaces_around_complex_component(c) + && !self.omit_spaces_around_complex_component(component) + { + self.buffer.push(b' '); + } + } + self.write_complex_selector_component(component); + last_component = Some(component); + } + } + + fn write_selector_list(&mut self, list: &SelectorList) { + let complexes = list.components.iter().filter(|c| !c.is_invisible()); + + let mut first = true; + + for complex in complexes { + if first { + first = false; + } else { + self.buffer.push(b','); + if complex.line_break { + self.buffer.push(b'\n'); + } else { + self.write_optional_space(); + } + } + self.write_complex_selector(complex); + } + } + + fn write_comma_separator(&mut self) { + self.buffer.push(b','); + self.write_optional_space(); + } + + fn visit_calculation(&mut self, calculation: &SassCalculation) -> SassResult<()> { + // todo: superfluous allocation + self.buffer + .extend_from_slice(calculation.name.to_string().as_bytes()); + self.buffer.push(b'('); + + if let Some((last, slice)) = calculation.args.split_last() { + for arg in slice { + self.write_calculation_arg(arg)?; + self.write_comma_separator(); + } + + self.write_calculation_arg(last)?; + } + + self.buffer.push(b')'); + + Ok(()) + } + + fn write_calculation_arg(&mut self, arg: &CalculationArg) -> SassResult<()> { + match arg { + CalculationArg::Number(num) => self.visit_number(num)?, + CalculationArg::Calculation(calc) => { + self.visit_calculation(calc)?; + } + CalculationArg::String(s) | CalculationArg::Interpolation(s) => { + self.buffer.extend_from_slice(s.as_bytes()); + } + CalculationArg::Operation { lhs, op, rhs } => { + let paren_left = match &**lhs { + CalculationArg::Interpolation(..) => true, + CalculationArg::Operation { op: op2, .. } => op2.precedence() < op.precedence(), + _ => false, + }; + + if paren_left { + self.buffer.push(b'('); + } + + self.write_calculation_arg(lhs)?; + + if paren_left { + self.buffer.push(b')'); + } + + let operator_whitespace = !self.options.is_compressed() || op.precedence() == 1; + + if operator_whitespace { + self.buffer.push(b' '); + } + + // todo: avoid allocation with `write_binary_operator` method + self.buffer.extend_from_slice(op.to_string().as_bytes()); + + if operator_whitespace { + self.buffer.push(b' '); + } + + let paren_right = match &**rhs { + CalculationArg::Interpolation(..) => true, + CalculationArg::Operation { op: op2, .. } => { + CalculationArg::parenthesize_calculation_rhs(*op, *op2) + } + _ => false, + }; + + if paren_right { + self.buffer.push(b'('); + } + + self.write_calculation_arg(rhs)?; + + if paren_right { + self.buffer.push(b')'); + } + } + } + + Ok(()) + } + + fn write_rgb(&mut self, color: &Color) { + let is_opaque = fuzzy_equals(color.alpha().0, 1.0); + + if is_opaque { + self.buffer.extend_from_slice(b"rgb("); + } else { + self.buffer.extend_from_slice(b"rgba("); + } + + self.write_float(color.red().0); + self.buffer.extend_from_slice(b", "); + self.write_float(color.green().0); + self.buffer.extend_from_slice(b", "); + self.write_float(color.blue().0); + + if !is_opaque { + self.buffer.extend_from_slice(b", "); + self.write_float(color.alpha().0); + } + + self.buffer.push(b')'); + } + + fn write_hsl(&mut self, color: &Color) { + let is_opaque = fuzzy_equals(color.alpha().0, 1.0); + + if is_opaque { + self.buffer.extend_from_slice(b"hsl("); + } else { + self.buffer.extend_from_slice(b"hsla("); + } + + self.write_float(color.hue().0); + self.buffer.extend_from_slice(b"deg, "); + self.write_float(color.saturation().0); + self.buffer.extend_from_slice(b"%, "); + self.write_float(color.lightness().0); + self.buffer.extend_from_slice(b"%"); + + if !is_opaque { + self.buffer.extend_from_slice(b", "); + self.write_float(color.alpha().0); + } + + self.buffer.push(b')'); + } + + fn write_hex_component(&mut self, channel: u32) { + debug_assert!(channel < 256); + + self.buffer.push(hex_char_for(channel >> 4) as u8); + self.buffer.push(hex_char_for(channel & 0xF) as u8); + } + + fn is_symmetrical_hex(channel: u32) -> bool { + channel & 0xF == channel >> 4 + } + + fn can_use_short_hex(color: &Color) -> bool { + Self::is_symmetrical_hex(color.red().0.round() as u32) + && Self::is_symmetrical_hex(color.green().0.round() as u32) + && Self::is_symmetrical_hex(color.blue().0.round() as u32) + } + + pub fn visit_color(&mut self, color: &Color) { + let red = color.red().0.round() as u8; + let green = color.green().0.round() as u8; + let blue = color.blue().0.round() as u8; + + let name = if fuzzy_equals(color.alpha().0, 1.0) { + NAMED_COLORS.get_by_rgba([red, green, blue]) + } else { + None + }; + + #[allow(clippy::unnecessary_unwrap)] + if self.options.is_compressed() { + if fuzzy_equals(color.alpha().0, 1.0) { + let hex_length = if Self::can_use_short_hex(color) { 4 } else { 7 }; + if name.is_some() && name.unwrap().len() <= hex_length { + self.buffer.extend_from_slice(name.unwrap().as_bytes()); + } else if Self::can_use_short_hex(color) { + self.buffer.push(b'#'); + self.buffer.push(hex_char_for(red as u32 & 0xF) as u8); + self.buffer.push(hex_char_for(green as u32 & 0xF) as u8); + self.buffer.push(hex_char_for(blue as u32 & 0xF) as u8); + } else { + self.buffer.push(b'#'); + self.write_hex_component(red as u32); + self.write_hex_component(green as u32); + self.write_hex_component(blue as u32); + } + } else { + self.write_rgb(color); + } + } else if color.format != ColorFormat::Infer { + match &color.format { + ColorFormat::Rgb => self.write_rgb(color), + ColorFormat::Hsl => self.write_hsl(color), + ColorFormat::Literal(text) => self.buffer.extend_from_slice(text.as_bytes()), + ColorFormat::Infer => unreachable!(), + } + // Always emit generated transparent colors in rgba format. This works + // around an IE bug. See sass/sass#1782. + } else if name.is_some() && !fuzzy_equals(color.alpha().0, 0.0) { + self.buffer.extend_from_slice(name.unwrap().as_bytes()); + } else if fuzzy_equals(color.alpha().0, 1.0) { + self.buffer.push(b'#'); + self.write_hex_component(red as u32); + self.write_hex_component(green as u32); + self.write_hex_component(blue as u32); + } else { + self.write_rgb(color); + } + } + + fn write_media_query(&mut self, query: &MediaQuery) { + if let Some(modifier) = &query.modifier { + self.buffer.extend_from_slice(modifier.as_bytes()); + self.buffer.push(b' '); + } + + if let Some(media_type) = &query.media_type { + self.buffer.extend_from_slice(media_type.as_bytes()); + + if !query.conditions.is_empty() { + self.buffer.extend_from_slice(b" and "); + } + } + + if query.conditions.len() == 1 && query.conditions.first().unwrap().starts_with("(not ") { + self.buffer.extend_from_slice(b"not "); + let condition = query.conditions.first().unwrap(); + self.buffer + .extend_from_slice(condition["(not ".len()..condition.len() - 1].as_bytes()); + } else { + let operator = if query.conjunction { " and " } else { " or " }; + self.buffer + .extend_from_slice(query.conditions.join(operator).as_bytes()); + } + } + + pub fn visit_number(&mut self, number: &SassNumber) -> SassResult<()> { + if let Some(as_slash) = &number.as_slash { + self.visit_number(&as_slash.0)?; + self.buffer.push(b'/'); + self.visit_number(&as_slash.1)?; + return Ok(()); + } + + if !self.inspect && number.unit.is_complex() { + return Err(( + format!( + "{} isn't a valid CSS value.", + inspect_number(number, self.options, self.span)? + ), + self.span, + ) + .into()); + } + + self.write_float(number.num.0); + write!(&mut self.buffer, "{}", number.unit)?; + + Ok(()) + } + + fn write_float(&mut self, float: f64) { + if float.is_infinite() && float.is_sign_negative() { + self.buffer.extend_from_slice(b"-Infinity"); + return; + } else if float.is_infinite() { + self.buffer.extend_from_slice(b"Infinity"); + return; + } + + // todo: can optimize away intermediate buffer + let mut buffer = String::with_capacity(3); + + if float < 0.0 { + buffer.push('-'); + } + + let num = float.abs(); + + if self.options.is_compressed() && num < 1.0 { + buffer.push_str( + format!("{:.10}", num)[1..] + .trim_end_matches('0') + .trim_end_matches('.'), + ); + } else { + buffer.push_str( + format!("{:.10}", num) + .trim_end_matches('0') + .trim_end_matches('.'), + ); + } + + if buffer.is_empty() || buffer == "-" || buffer == "-0" { + buffer = "0".to_owned(); + } + + self.buffer.append(&mut buffer.into_bytes()); + } + + pub fn visit_group( + &mut self, + stmt: CssStmt, + prev_was_group_end: bool, + prev_requires_semicolon: bool, + ) -> SassResult<()> { + if prev_requires_semicolon { + self.buffer.push(b';'); + } + + if !self.buffer.is_empty() { + self.write_optional_newline(); + } + + if prev_was_group_end && !self.buffer.is_empty() { + self.write_optional_newline(); + } + + self.visit_stmt(stmt)?; + + Ok(()) + } + + fn finish_for_expr(self) -> String { + // SAFETY: todo + unsafe { String::from_utf8_unchecked(self.buffer) } + } + + pub fn finish(mut self, prev_requires_semicolon: bool) -> String { + let is_not_ascii = self.buffer.iter().any(|&c| !c.is_ascii()); + + if prev_requires_semicolon { + self.buffer.push(b';'); + } + + if !self.buffer.is_empty() { + self.write_optional_newline(); + } + + // SAFETY: todo + let mut as_string = unsafe { String::from_utf8_unchecked(self.buffer) }; + + if is_not_ascii && self.options.is_compressed() { + as_string.insert(0, '\u{FEFF}'); + } else if is_not_ascii { + as_string.insert_str(0, "@charset \"UTF-8\";\n"); + } + + as_string + } + + fn write_indentation(&mut self) { + if self.options.is_compressed() { + return; + } + + self.buffer.reserve(self.indentation); + for _ in 0..self.indentation { + self.buffer.push(b' '); + } + } + + fn visit_value(&mut self, value: Spanned) -> SassResult<()> { + match value.node { + Value::Dimension(num) => self.visit_number(&num)?, + Value::Color(color) => self.visit_color(&color), + Value::Calculation(calc) => self.visit_calculation(&calc)?, + _ => { + let value_as_str = value + .node + .to_css_string(value.span, self.options.is_compressed())?; + self.buffer.extend_from_slice(value_as_str.as_bytes()); + } + } + + Ok(()) + } + + fn write_style(&mut self, style: Style) -> SassResult<()> { + if !self.options.is_compressed() { + self.write_indentation(); + } + + self.buffer + .extend_from_slice(style.property.resolve_ref().as_bytes()); + self.buffer.push(b':'); + + // todo: _writeFoldedValue and _writeReindentedValue + if !style.declared_as_custom_property && !self.options.is_compressed() { + self.buffer.push(b' '); + } + + self.visit_value(*style.value)?; + + Ok(()) + } + + fn write_import(&mut self, import: &str, modifiers: Option) -> SassResult<()> { + self.write_indentation(); + self.buffer.extend_from_slice(b"@import "); + write!(&mut self.buffer, "{}", import)?; + + if let Some(modifiers) = modifiers { + self.buffer.push(b' '); + self.buffer.extend_from_slice(modifiers.as_bytes()); + } + + Ok(()) + } + + fn write_comment(&mut self, comment: &str, span: Span) -> SassResult<()> { + if self.options.is_compressed() && !comment.starts_with("/*!") { + return Ok(()); + } + + self.write_indentation(); + let col = self.map.look_up_pos(span.low()).position.column; + let mut lines = comment.lines(); + + if let Some(line) = lines.next() { + self.buffer.extend_from_slice(line.trim_start().as_bytes()); + } + + let lines = lines + .map(|line| { + let diff = (line.len() - line.trim_start().len()).saturating_sub(col); + format!("{}{}", " ".repeat(diff), line.trim_start()) + }) + .collect::>() + .join("\n"); + + if !lines.is_empty() { + write!(&mut self.buffer, "\n{}", lines)?; + } + + Ok(()) + } + + pub fn requires_semicolon(stmt: &CssStmt) -> bool { + match stmt { + CssStmt::Style(_) | CssStmt::Import(_, _) => true, + CssStmt::UnknownAtRule(rule, _) => !rule.has_body, + _ => false, + } + } + + fn write_children(&mut self, mut children: Vec) -> SassResult<()> { + if self.options.is_compressed() { + self.buffer.push(b'{'); + } else { + self.buffer.extend_from_slice(b" {\n"); + } + + self.indentation += self.indent_width; + + let last = children.pop(); + + for child in children { + let needs_semicolon = Self::requires_semicolon(&child); + let did_write = self.visit_stmt(child)?; + + if !did_write { + continue; + } + + if needs_semicolon { + self.buffer.push(b';'); + } + + self.write_optional_newline(); + } + + if let Some(last) = last { + let needs_semicolon = Self::requires_semicolon(&last); + let did_write = self.visit_stmt(last)?; + + if did_write { + if needs_semicolon && !self.options.is_compressed() { + self.buffer.push(b';'); + } + + self.write_optional_newline(); + } + } + + self.indentation -= self.indent_width; + + if self.options.is_compressed() { + self.buffer.push(b'}'); + } else { + self.write_indentation(); + self.buffer.extend_from_slice(b"}"); + } + + Ok(()) + } + + fn write_optional_space(&mut self) { + if !self.options.is_compressed() { + self.buffer.push(b' '); + } + } + + fn write_optional_newline(&mut self) { + if !self.options.is_compressed() { + self.buffer.push(b'\n'); + } + } + + fn write_supports_rule(&mut self, supports_rule: SupportsRule) -> SassResult<()> { + self.write_indentation(); + self.buffer.extend_from_slice(b"@supports"); + + if !supports_rule.params.is_empty() { + self.buffer.push(b' '); + self.buffer + .extend_from_slice(supports_rule.params.as_bytes()); + } + + self.write_children(supports_rule.body)?; + + Ok(()) + } + + /// Returns whether or not text was written + fn visit_stmt(&mut self, stmt: CssStmt) -> SassResult { + if stmt.is_invisible() { + return Ok(false); + } + + match stmt { + CssStmt::RuleSet { selector, body, .. } => { + self.write_indentation(); + self.write_selector_list(&selector.as_selector_list()); + + self.write_children(body)?; + } + CssStmt::Media(media_rule, ..) => { + self.write_indentation(); + self.buffer.extend_from_slice(b"@media "); + + if let Some((last, rest)) = media_rule.query.split_last() { + for query in rest { + self.write_media_query(query); + + self.buffer.push(b','); + + self.write_optional_space(); + } + + self.write_media_query(last); + } + + self.write_children(media_rule.body)?; + } + CssStmt::UnknownAtRule(unknown_at_rule, ..) => { + self.write_indentation(); + self.buffer.push(b'@'); + self.buffer + .extend_from_slice(unknown_at_rule.name.as_bytes()); + + if !unknown_at_rule.params.is_empty() { + write!(&mut self.buffer, " {}", unknown_at_rule.params)?; + } + + if !unknown_at_rule.has_body { + debug_assert!(unknown_at_rule.body.is_empty()); + return Ok(true); + } else if unknown_at_rule.body.iter().all(CssStmt::is_invisible) { + self.buffer.extend_from_slice(b" {}"); + return Ok(true); + } + + self.write_children(unknown_at_rule.body)?; + } + CssStmt::Style(style) => self.write_style(style)?, + CssStmt::Comment(comment, span) => self.write_comment(&comment, span)?, + CssStmt::KeyframesRuleSet(keyframes_rule_set) => { + self.write_indentation(); + // todo: i bet we can do something like write_with_separator to avoid extra allocation + let selector = keyframes_rule_set + .selector + .into_iter() + .map(|s| s.to_string()) + .collect::>() + .join(", "); + + self.buffer.extend_from_slice(selector.as_bytes()); + + self.write_children(keyframes_rule_set.body)?; + } + CssStmt::Import(import, modifier) => self.write_import(&import, modifier)?, + CssStmt::Supports(supports_rule, _) => self.write_supports_rule(supports_rule)?, + } + + Ok(true) + } +} diff --git a/src/style.rs b/src/style.rs deleted file mode 100644 index bbe91da9..00000000 --- a/src/style.rs +++ /dev/null @@ -1,20 +0,0 @@ -use codemap::Spanned; - -use crate::{error::SassResult, interner::InternedString, value::Value}; - -/// A style: `color: red` -#[derive(Clone, Debug)] -pub(crate) struct Style { - pub property: InternedString, - pub value: Box>, -} - -impl Style { - pub fn to_string(&self) -> SassResult { - Ok(format!( - "{}: {};", - self.property, - self.value.node.to_css_string(self.value.span, false)? - )) - } -} diff --git a/src/token.rs b/src/token.rs index ba9c979e..bdddad49 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,7 +1,7 @@ -use crate::utils::IsWhitespace; - use codemap::Span; +// todo: remove span from tokens + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) struct Token { pub pos: Span, @@ -17,13 +17,3 @@ impl Token { self.pos } } - -impl IsWhitespace for Token { - fn is_whitespace(&self) -> bool { - if self.kind.is_whitespace() { - return true; - } - - false - } -} diff --git a/src/unit/conversion.rs b/src/unit/conversion.rs index 8862d14b..c3a35d35 100644 --- a/src/unit/conversion.rs +++ b/src/unit/conversion.rs @@ -2,132 +2,135 @@ //! //! Arbitrary precision is retained. -use std::{collections::HashMap, f64::consts::PI}; +use std::{ + collections::{HashMap, HashSet}, + f64::consts::PI, + iter::FromIterator, +}; -use num_traits::One; use once_cell::sync::Lazy; -use crate::{unit::Unit, value::Number}; +use crate::unit::Unit; -pub(crate) static UNIT_CONVERSION_TABLE: Lazy>> = +pub(crate) static UNIT_CONVERSION_TABLE: Lazy>> = Lazy::new(|| { let mut from_in = HashMap::new(); - from_in.insert(Unit::In, Number::one()); - from_in.insert(Unit::Cm, Number::one() / Number::from(2.54)); - from_in.insert(Unit::Pc, Number::small_ratio(1, 6)); - from_in.insert(Unit::Mm, Number::one() / Number::from(25.4)); - from_in.insert(Unit::Q, Number::one() / Number::from(101.6)); - from_in.insert(Unit::Pt, Number::small_ratio(1, 72)); - from_in.insert(Unit::Px, Number::small_ratio(1, 96)); + from_in.insert(Unit::In, 1.0); + from_in.insert(Unit::Cm, 1.0 / 2.54); + from_in.insert(Unit::Pc, 1.0 / 6.0); + from_in.insert(Unit::Mm, 1.0 / 25.4); + from_in.insert(Unit::Q, 1.0 / 101.6); + from_in.insert(Unit::Pt, 1.0 / 72.0); + from_in.insert(Unit::Px, 1.0 / 96.0); let mut from_cm = HashMap::new(); - from_cm.insert(Unit::In, Number::from(2.54)); - from_cm.insert(Unit::Cm, Number::one()); - from_cm.insert(Unit::Pc, Number::from(2.54) / Number::from(6)); - from_cm.insert(Unit::Mm, Number::small_ratio(1, 10)); - from_cm.insert(Unit::Q, Number::small_ratio(1, 40)); - from_cm.insert(Unit::Pt, Number::from(2.54) / Number::from(72)); - from_cm.insert(Unit::Px, Number::from(2.54) / Number::from(96)); + from_cm.insert(Unit::In, 2.54); + from_cm.insert(Unit::Cm, 1.0); + from_cm.insert(Unit::Pc, 2.54 / 6.0); + from_cm.insert(Unit::Mm, 1.0 / 10.0); + from_cm.insert(Unit::Q, 1.0 / 40.0); + from_cm.insert(Unit::Pt, 2.54 / 72.0); + from_cm.insert(Unit::Px, 2.54 / 96.0); let mut from_pc = HashMap::new(); - from_pc.insert(Unit::In, Number::from(6)); - from_pc.insert(Unit::Cm, Number::from(6) / Number::from(2.54)); - from_pc.insert(Unit::Pc, Number::one()); - from_pc.insert(Unit::Mm, Number::from(6) / Number::from(25.4)); - from_pc.insert(Unit::Q, Number::from(6) / Number::from(101.6)); - from_pc.insert(Unit::Pt, Number::small_ratio(1, 12)); - from_pc.insert(Unit::Px, Number::small_ratio(1, 16)); + from_pc.insert(Unit::In, 6.0); + from_pc.insert(Unit::Cm, 6.0 / 2.54); + from_pc.insert(Unit::Pc, 1.0); + from_pc.insert(Unit::Mm, 6.0 / 25.4); + from_pc.insert(Unit::Q, 6.0 / 101.6); + from_pc.insert(Unit::Pt, 1.0 / 12.0); + from_pc.insert(Unit::Px, 1.0 / 16.0); let mut from_mm = HashMap::new(); - from_mm.insert(Unit::In, Number::from(25.4)); - from_mm.insert(Unit::Cm, Number::from(10)); - from_mm.insert(Unit::Pc, Number::from(25.4) / Number::from(6)); - from_mm.insert(Unit::Mm, Number::one()); - from_mm.insert(Unit::Q, Number::small_ratio(1, 4)); - from_mm.insert(Unit::Pt, Number::from(25.4) / Number::from(72)); - from_mm.insert(Unit::Px, Number::from(25.4) / Number::from(96)); + from_mm.insert(Unit::In, 25.4); + from_mm.insert(Unit::Cm, 10.0); + from_mm.insert(Unit::Pc, 25.4 / 6.0); + from_mm.insert(Unit::Mm, 1.0); + from_mm.insert(Unit::Q, 1.0 / 4.0); + from_mm.insert(Unit::Pt, 25.4 / 72.0); + from_mm.insert(Unit::Px, 25.4 / 96.0); let mut from_q = HashMap::new(); - from_q.insert(Unit::In, Number::from(101.6)); - from_q.insert(Unit::Cm, Number::from(40)); - from_q.insert(Unit::Pc, Number::from(101.6) / Number::from(6)); - from_q.insert(Unit::Mm, Number::from(4)); - from_q.insert(Unit::Q, Number::one()); - from_q.insert(Unit::Pt, Number::from(101.6) / Number::from(72)); - from_q.insert(Unit::Px, Number::from(101.6) / Number::from(96)); + from_q.insert(Unit::In, 101.6); + from_q.insert(Unit::Cm, 40.0); + from_q.insert(Unit::Pc, 101.6 / 6.0); + from_q.insert(Unit::Mm, 4.0); + from_q.insert(Unit::Q, 1.0); + from_q.insert(Unit::Pt, 101.6 / 72.0); + from_q.insert(Unit::Px, 101.6 / 96.0); let mut from_pt = HashMap::new(); - from_pt.insert(Unit::In, Number::from(72)); - from_pt.insert(Unit::Cm, Number::from(72) / Number::from(2.54)); - from_pt.insert(Unit::Pc, Number::from(12)); - from_pt.insert(Unit::Mm, Number::from(72) / Number::from(25.4)); - from_pt.insert(Unit::Q, Number::from(72) / Number::from(101.6)); - from_pt.insert(Unit::Pt, Number::one()); - from_pt.insert(Unit::Px, Number::small_ratio(3, 4)); + from_pt.insert(Unit::In, 72.0); + from_pt.insert(Unit::Cm, 72.0 / 2.54); + from_pt.insert(Unit::Pc, 12.0); + from_pt.insert(Unit::Mm, 72.0 / 25.4); + from_pt.insert(Unit::Q, 72.0 / 101.6); + from_pt.insert(Unit::Pt, 1.0); + from_pt.insert(Unit::Px, 3.0 / 4.0); let mut from_px = HashMap::new(); - from_px.insert(Unit::In, Number::from(96)); - from_px.insert(Unit::Cm, Number::from(96) / Number::from(2.54)); - from_px.insert(Unit::Pc, Number::from(16)); - from_px.insert(Unit::Mm, Number::from(96) / Number::from(25.4)); - from_px.insert(Unit::Q, Number::from(96) / Number::from(101.6)); - from_px.insert(Unit::Pt, Number::small_ratio(4, 3)); - from_px.insert(Unit::Px, Number::one()); + from_px.insert(Unit::In, 96.0); + from_px.insert(Unit::Cm, 96.0 / 2.54); + from_px.insert(Unit::Pc, 16.0); + from_px.insert(Unit::Mm, 96.0 / 25.4); + from_px.insert(Unit::Q, 96.0 / 101.6); + from_px.insert(Unit::Pt, 4.0 / 3.0); + from_px.insert(Unit::Px, 1.0); let mut from_deg = HashMap::new(); - from_deg.insert(Unit::Deg, Number::one()); - from_deg.insert(Unit::Grad, Number::small_ratio(9, 10)); - from_deg.insert(Unit::Rad, Number::from(180) / Number::from(PI)); - from_deg.insert(Unit::Turn, Number::from(360)); + from_deg.insert(Unit::Deg, 1.0); + from_deg.insert(Unit::Grad, 9.0 / 10.0); + from_deg.insert(Unit::Rad, 180.0 / PI); + from_deg.insert(Unit::Turn, 360.0); let mut from_grad = HashMap::new(); - from_grad.insert(Unit::Deg, Number::small_ratio(10, 9)); - from_grad.insert(Unit::Grad, Number::one()); - from_grad.insert(Unit::Rad, Number::from(200) / Number::from(PI)); - from_grad.insert(Unit::Turn, Number::from(400)); + from_grad.insert(Unit::Deg, 10.0 / 9.0); + from_grad.insert(Unit::Grad, 1.0); + from_grad.insert(Unit::Rad, 200.0 / PI); + from_grad.insert(Unit::Turn, 400.0); let mut from_rad = HashMap::new(); - from_rad.insert(Unit::Deg, Number::from(PI) / Number::from(180)); - from_rad.insert(Unit::Grad, Number::from(PI) / Number::from(200)); - from_rad.insert(Unit::Rad, Number::one()); - from_rad.insert(Unit::Turn, Number::from(2.0 * PI)); + from_rad.insert(Unit::Deg, PI / 180.0); + from_rad.insert(Unit::Grad, PI / 200.0); + from_rad.insert(Unit::Rad, 1.0); + from_rad.insert(Unit::Turn, 2.0 * PI); let mut from_turn = HashMap::new(); - from_turn.insert(Unit::Deg, Number::small_ratio(1, 360)); - from_turn.insert(Unit::Grad, Number::small_ratio(1, 400)); - from_turn.insert(Unit::Rad, Number::one() / Number::from(2.0 * PI)); - from_turn.insert(Unit::Turn, Number::one()); + from_turn.insert(Unit::Deg, 1.0 / 360.0); + from_turn.insert(Unit::Grad, 1.0 / 400.0); + from_turn.insert(Unit::Rad, 1.0 / (2.0 * PI)); + from_turn.insert(Unit::Turn, 1.0); let mut from_s = HashMap::new(); - from_s.insert(Unit::S, Number::one()); - from_s.insert(Unit::Ms, Number::small_ratio(1, 1000)); + from_s.insert(Unit::S, 1.0); + from_s.insert(Unit::Ms, 1.0 / 1000.0); let mut from_ms = HashMap::new(); - from_ms.insert(Unit::S, Number::from(1000)); - from_ms.insert(Unit::Ms, Number::one()); + from_ms.insert(Unit::S, 1000.0); + from_ms.insert(Unit::Ms, 1.0); let mut from_hz = HashMap::new(); - from_hz.insert(Unit::Hz, Number::one()); - from_hz.insert(Unit::Khz, Number::from(1000)); + from_hz.insert(Unit::Hz, 1.0); + from_hz.insert(Unit::Khz, 1000.0); let mut from_khz = HashMap::new(); - from_khz.insert(Unit::Hz, Number::small_ratio(1, 1000)); - from_khz.insert(Unit::Khz, Number::one()); + from_khz.insert(Unit::Hz, 1.0 / 1000.0); + from_khz.insert(Unit::Khz, 1.0); let mut from_dpi = HashMap::new(); - from_dpi.insert(Unit::Dpi, Number::one()); - from_dpi.insert(Unit::Dpcm, Number::from(2.54)); - from_dpi.insert(Unit::Dppx, Number::from(96)); + from_dpi.insert(Unit::Dpi, 1.0); + from_dpi.insert(Unit::Dpcm, 2.54); + from_dpi.insert(Unit::Dppx, 96.0); let mut from_dpcm = HashMap::new(); - from_dpcm.insert(Unit::Dpi, Number::one() / Number::from(2.54)); - from_dpcm.insert(Unit::Dpcm, Number::one()); - from_dpcm.insert(Unit::Dppx, Number::from(96) / Number::from(2.54)); + from_dpcm.insert(Unit::Dpi, 1.0 / 2.54); + from_dpcm.insert(Unit::Dpcm, 1.0); + from_dpcm.insert(Unit::Dppx, 96.0 / 2.54); let mut from_dppx = HashMap::new(); - from_dppx.insert(Unit::Dpi, Number::small_ratio(1, 96)); - from_dppx.insert(Unit::Dpcm, Number::from(2.54) / Number::from(96)); - from_dppx.insert(Unit::Dppx, Number::one()); + from_dppx.insert(Unit::Dpi, 1.0 / 96.0); + from_dppx.insert(Unit::Dpcm, 2.54 / 96.0); + from_dppx.insert(Unit::Dppx, 1.0); let mut m = HashMap::new(); m.insert(Unit::In, from_in); @@ -155,3 +158,54 @@ pub(crate) static UNIT_CONVERSION_TABLE: Lazy; 5]> = Lazy::new(|| { + let dimensions = HashSet::from_iter([ + Unit::Em, + Unit::Ex, + Unit::Ch, + Unit::Rem, + Unit::Vw, + Unit::Vh, + Unit::Vmin, + Unit::Vmax, + Unit::Cm, + Unit::Mm, + Unit::Q, + Unit::In, + Unit::Pt, + Unit::Pc, + Unit::Px, + ]); + let angles = HashSet::from_iter([Unit::Deg, Unit::Grad, Unit::Rad, Unit::Turn]); + let time = HashSet::from_iter([Unit::S, Unit::Ms]); + let frequency = HashSet::from_iter([Unit::Hz, Unit::Khz]); + let resolution = HashSet::from_iter([Unit::Dpi, Unit::Dpcm, Unit::Dppx]); + + [dimensions, angles, time, frequency, resolution] +}); + +pub(crate) fn known_compatibilities_by_unit(unit: &Unit) -> Option<&HashSet> { + match unit { + Unit::Em + | Unit::Ex + | Unit::Ch + | Unit::Rem + | Unit::Vw + | Unit::Vh + | Unit::Vmin + | Unit::Vmax + | Unit::Cm + | Unit::Mm + | Unit::Q + | Unit::In + | Unit::Pt + | Unit::Pc + | Unit::Px => Some(&KNOWN_COMPATIBILITIES[0]), + Unit::Deg | Unit::Grad | Unit::Rad | Unit::Turn => Some(&KNOWN_COMPATIBILITIES[1]), + Unit::S | Unit::Ms => Some(&KNOWN_COMPATIBILITIES[2]), + Unit::Hz | Unit::Khz => Some(&KNOWN_COMPATIBILITIES[3]), + Unit::Dpi | Unit::Dpcm | Unit::Dppx => Some(&KNOWN_COMPATIBILITIES[4]), + _ => None, + } +} diff --git a/src/unit/mod.rs b/src/unit/mod.rs index 112c50b8..7ff26b1e 100644 --- a/src/unit/mod.rs +++ b/src/unit/mod.rs @@ -1,11 +1,8 @@ -use std::{ - fmt, - ops::{Div, Mul}, -}; +use std::fmt; use crate::interner::InternedString; -pub(crate) use conversion::UNIT_CONVERSION_TABLE; +pub(crate) use conversion::{known_compatibilities_by_unit, UNIT_CONVERSION_TABLE}; mod conversion; @@ -92,8 +89,6 @@ pub(crate) enum Unit { Dpcm, /// Represents the number of dots per px unit Dppx, - /// Alias for dppx - X, // Other units /// Represents a fraction of the available space in the grid container @@ -105,14 +100,24 @@ pub(crate) enum Unit { /// Unspecified unit None, - /// Units multiplied together - /// Boxed under the assumption that mul units are exceedingly rare - #[allow(clippy::box_collection)] - Mul(Box>), + Complex { + numer: Vec, + denom: Vec, + }, +} + +pub(crate) fn are_any_convertible(units1: &[Unit], units2: &[Unit]) -> bool { + for unit1 in units1 { + for unit2 in units2 { + if unit1.comparable(unit2) { + return true; + } + } + } - /// Units divided by each other - Div(Box), + false } + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum UnitKind { Absolute, @@ -126,115 +131,35 @@ pub(crate) enum UnitKind { None, } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub(crate) struct DivUnit { - numer: Unit, - denom: Unit, -} - -impl DivUnit { - pub const fn new(numer: Unit, denom: Unit) -> Self { - Self { numer, denom } - } -} - -impl fmt::Display for DivUnit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.numer == Unit::None { - write!(f, "{}^-1", self.denom) +impl Unit { + pub fn new(mut numer: Vec, denom: Vec) -> Self { + if denom.is_empty() && numer.is_empty() { + Unit::None + } else if denom.is_empty() && numer.len() == 1 { + numer.pop().unwrap() } else { - write!(f, "{}/{}", self.numer, self.denom) + Unit::Complex { numer, denom } } } -} -#[allow(clippy::match_same_arms)] -impl Mul for DivUnit { - type Output = Unit; - fn mul(self, rhs: Unit) -> Self::Output { - match rhs { - Unit::Mul(..) => todo!(), - Unit::Div(..) => todo!(), - Unit::None => todo!(), - _ => { - if self.denom == rhs { - self.numer - } else { - match self.denom { - Unit::Mul(..) => todo!(), - Unit::Div(..) => unreachable!(), - _ => match self.numer { - Unit::Mul(..) => todo!(), - Unit::Div(..) => unreachable!(), - Unit::None => { - let numer = Unit::Mul(Box::new(vec![rhs])); - Unit::Div(Box::new(DivUnit::new(numer, self.denom))) - } - _ => { - let numer = Unit::Mul(Box::new(vec![self.numer, rhs])); - Unit::Div(Box::new(DivUnit::new(numer, self.denom))) - } - }, - } - } - } + pub fn numer_and_denom(self) -> (Vec, Vec) { + match self { + Self::Complex { numer, denom } => (numer, denom), + Self::None => (Vec::new(), Vec::new()), + v => (vec![v], Vec::new()), } } -} -// impl Div for DivUnit { -// type Output = Unit; -// fn div(self, rhs: Unit) -> Self::Output { -// todo!() -// } -// } + pub fn invert(self) -> Self { + let (numer, denom) = self.numer_and_denom(); -impl Mul for Unit { - type Output = Unit; - fn mul(self, rhs: Unit) -> Self::Output { - match self { - Unit::Mul(u) => match rhs { - Unit::Mul(u2) => { - let mut unit1 = *u; - unit1.extend_from_slice(&u2); - Unit::Mul(Box::new(unit1)) - } - Unit::Div(..) => todo!(), - _ => { - let mut unit1 = *u; - unit1.push(rhs); - Unit::Mul(Box::new(unit1)) - } - }, - Unit::Div(div) => *div * rhs, - _ => match rhs { - Unit::Mul(u2) => { - let mut unit1 = vec![self]; - unit1.extend_from_slice(&u2); - Unit::Mul(Box::new(unit1)) - } - Unit::Div(..) => todo!(), - _ => Unit::Mul(Box::new(vec![self, rhs])), - }, - } + Self::new(denom, numer) } -} -impl Div for Unit { - type Output = Unit; - #[allow(clippy::if_same_then_else)] - fn div(self, rhs: Unit) -> Self::Output { - if let Unit::Div(..) = self { - todo!() - } else if let Unit::Div(..) = rhs { - todo!() - } else { - Unit::Div(Box::new(DivUnit::new(self, rhs))) - } + pub fn is_complex(&self) -> bool { + matches!(self, Unit::Complex { numer, denom } if numer.len() != 1 || !denom.is_empty()) } -} -impl Unit { pub fn comparable(&self, other: &Unit) -> bool { if other == &Unit::None { return true; @@ -266,11 +191,9 @@ impl Unit { Unit::Deg | Unit::Grad | Unit::Rad | Unit::Turn => UnitKind::Angle, Unit::S | Unit::Ms => UnitKind::Time, Unit::Hz | Unit::Khz => UnitKind::Frequency, - Unit::Dpi | Unit::Dpcm | Unit::Dppx | Unit::X => UnitKind::Resolution, + Unit::Dpi | Unit::Dpcm | Unit::Dppx => UnitKind::Resolution, Unit::None => UnitKind::None, - Unit::Fr | Unit::Percent | Unit::Unknown(..) | Unit::Mul(..) | Unit::Div(..) => { - UnitKind::Other - } + Unit::Fr | Unit::Percent | Unit::Unknown(..) | Unit::Complex { .. } => UnitKind::Other, } } } @@ -311,7 +234,6 @@ impl From for Unit { "dpi" => Unit::Dpi, "dpcm" => Unit::Dpcm, "dppx" => Unit::Dppx, - "x" => Unit::X, "fr" => Unit::Fr, _ => Unit::Unknown(InternedString::get_or_intern(unit)), } @@ -354,19 +276,37 @@ impl fmt::Display for Unit { Unit::Dpi => write!(f, "dpi"), Unit::Dpcm => write!(f, "dpcm"), Unit::Dppx => write!(f, "dppx"), - Unit::X => write!(f, "x"), Unit::Fr => write!(f, "fr"), Unit::Unknown(s) => write!(f, "{}", s), Unit::None => Ok(()), - Unit::Mul(u) => write!( - f, - "{}", - u.iter() + Unit::Complex { numer, denom } => { + debug_assert!( + numer.len() > 1 || !denom.is_empty(), + "unsimplified complex unit" + ); + + let numer_rendered = numer + .iter() + .map(ToString::to_string) + .collect::>() + .join("*"); + + let denom_rendered = denom + .iter() .map(ToString::to_string) .collect::>() - .join("*") - ), - Unit::Div(u) => write!(f, "{}", u), + .join("*"); + + if denom.is_empty() { + write!(f, "{}", numer_rendered) + } else if numer.is_empty() && denom.len() == 1 { + write!(f, "{}^-1", denom_rendered) + } else if numer.is_empty() { + write!(f, "({})^-1", denom_rendered) + } else { + write!(f, "{}/{}", numer_rendered, denom_rendered) + } + } } } } diff --git a/src/utils/chars.rs b/src/utils/chars.rs index d8af8747..afa13667 100644 --- a/src/utils/chars.rs +++ b/src/utils/chars.rs @@ -13,12 +13,7 @@ pub(crate) fn is_name(c: char) -> bool { } pub(crate) fn is_name_start(c: char) -> bool { - // NOTE: in the dart-sass implementation, identifiers cannot start - // with numbers. We explicitly differentiate from the reference - // implementation here in order to support selectors beginning with numbers. - // This can be considered a hack and in the future it would be nice to refactor - // how this is handled. - c == '_' || c.is_alphanumeric() || c as u32 >= 0x0080 + c == '_' || c.is_alphabetic() || c as u32 >= 0x0080 } pub(crate) fn as_hex(c: char) -> u32 { diff --git a/src/utils/comment_whitespace.rs b/src/utils/comment_whitespace.rs deleted file mode 100644 index f3bbfbb1..00000000 --- a/src/utils/comment_whitespace.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::lexer::Lexer; - -pub(crate) trait IsWhitespace { - fn is_whitespace(&self) -> bool; -} - -impl IsWhitespace for char { - fn is_whitespace(&self) -> bool { - self.is_ascii_whitespace() - } -} - -pub(crate) fn devour_whitespace(s: &mut Lexer) -> bool { - let mut found_whitespace = false; - while let Some(w) = s.peek() { - if !w.is_whitespace() { - break; - } - found_whitespace = true; - s.next(); - } - found_whitespace -} - -/// Eat tokens until a newline -/// -/// This exists largely to eat silent comments, "//" -/// We only have to check for \n as the lexing step normalizes all newline characters -/// -/// The newline is consumed -pub(crate) fn read_until_newline(toks: &mut Lexer) { - for tok in toks { - if tok.kind == '\n' { - return; - } - } -} diff --git a/src/utils/map_view.rs b/src/utils/map_view.rs new file mode 100644 index 00000000..e0a33fff --- /dev/null +++ b/src/utils/map_view.rs @@ -0,0 +1,345 @@ +use std::{ + cell::RefCell, + collections::{BTreeMap, HashSet}, + fmt, + sync::Arc, +}; + +use crate::common::Identifier; + +pub(crate) trait MapView: fmt::Debug { + type Value; + fn get(&self, name: Identifier) -> Option; + fn remove(&self, name: Identifier) -> Option; + fn insert(&self, name: Identifier, value: Self::Value) -> Option; + fn len(&self) -> usize; + fn is_empty(&self) -> bool { + self.len() == 0 + } + fn contains_key(&self, k: Identifier) -> bool { + self.get(k).is_some() + } + // todo: wildly ineffecient to return vec here, because of the arbitrary nesting of Self + fn keys(&self) -> Vec; + fn iter(&self) -> Vec<(Identifier, Self::Value)>; +} + +impl MapView for Arc> { + type Value = T; + fn get(&self, name: Identifier) -> Option { + (**self).get(name) + } + fn remove(&self, name: Identifier) -> Option { + (**self).remove(name) + } + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + (**self).insert(name, value) + } + fn len(&self) -> usize { + (**self).len() + } + fn keys(&self) -> Vec { + (**self).keys() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + (**self).iter() + } +} + +#[derive(Debug)] +pub(crate) struct BaseMapView(pub Arc>>); + +impl Clone for BaseMapView { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct UnprefixedMapView + Clone>( + pub T, + pub String, +); + +#[derive(Debug, Clone)] +pub(crate) struct PrefixedMapView + Clone>( + pub T, + pub String, +); + +impl MapView for BaseMapView { + type Value = T; + fn get(&self, name: Identifier) -> Option { + (*self.0).borrow().get(&name).cloned() + } + + fn len(&self) -> usize { + (*self.0).borrow().len() + } + + fn remove(&self, name: Identifier) -> Option { + (*self.0).borrow_mut().remove(&name) + } + + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + (*self.0).borrow_mut().insert(name, value) + } + + fn keys(&self) -> Vec { + (*self.0).borrow().keys().copied().collect() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + (*self.0).borrow().clone().into_iter().collect() + } +} + +impl + Clone> MapView for UnprefixedMapView { + type Value = V; + fn get(&self, name: Identifier) -> Option { + let name = Identifier::from(format!("{}{}", self.1, name)); + self.0.get(name) + } + + fn remove(&self, name: Identifier) -> Option { + let name = Identifier::from(format!("{}{}", self.1, name)); + self.0.remove(name) + } + + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + let name = Identifier::from(format!("{}{}", self.1, name)); + self.0.insert(name, value) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn keys(&self) -> Vec { + self.0 + .keys() + .into_iter() + .filter(|key| key.as_str().starts_with(&self.1)) + .map(|key| Identifier::from(key.as_str().strip_prefix(&self.1).unwrap())) + .collect() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + unimplemented!() + } +} + +impl + Clone> MapView for PrefixedMapView { + type Value = V; + fn get(&self, name: Identifier) -> Option { + if !name.as_str().starts_with(&self.1) { + return None; + } + + let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); + + self.0.get(name) + } + + fn remove(&self, name: Identifier) -> Option { + if !name.as_str().starts_with(&self.1) { + return None; + } + + let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); + + self.0.remove(name) + } + + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + if !name.as_str().starts_with(&self.1) { + return None; + } + + let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); + + self.0.insert(name, value) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn keys(&self) -> Vec { + self.0 + .keys() + .into_iter() + .filter(|key| key.as_str().starts_with(&self.1)) + .map(|key| Identifier::from(format!("{}{}", self.1, key))) + .collect() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + unimplemented!() + } +} + +#[derive(Debug, Clone)] +pub(crate) struct LimitedMapView + Clone>( + pub T, + pub HashSet, +); + +impl + Clone> LimitedMapView { + pub fn safelist(map: T, keys: &HashSet) -> Self { + let keys = keys + .iter() + .copied() + .filter(|key| map.contains_key(*key)) + .collect(); + + Self(map, keys) + } + + pub fn blocklist(map: T, keys: &HashSet) -> Self { + let keys = keys + .iter() + .copied() + .filter(|key| !map.contains_key(*key)) + .collect(); + + Self(map, keys) + } +} + +impl + Clone> MapView for LimitedMapView { + type Value = V; + fn get(&self, name: Identifier) -> Option { + if !self.1.contains(&name) { + return None; + } + + self.0.get(name) + } + + fn remove(&self, name: Identifier) -> Option { + if !self.1.contains(&name) { + return None; + } + + self.0.remove(name) + } + + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + if !self.1.contains(&name) { + return None; + } + + self.0.insert(name, value) + } + + fn len(&self) -> usize { + self.1.len() + } + + fn keys(&self) -> Vec { + self.1.iter().copied().collect() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + unimplemented!() + } +} + +#[derive(Debug)] +pub(crate) struct MergedMapView( + pub Vec>>, + HashSet, +); + +impl MergedMapView { + pub fn new(maps: Vec>>) -> Self { + let unique_keys: HashSet = maps.iter().fold(HashSet::new(), |mut keys, map| { + keys.extend(&map.keys()); + keys + }); + + Self(maps, unique_keys) + } +} + +impl MapView for MergedMapView { + type Value = V; + fn get(&self, name: Identifier) -> Option { + self.0.iter().rev().find_map(|map| (*map).get(name)) + } + + fn remove(&self, _name: Identifier) -> Option { + unimplemented!() + } + + fn len(&self) -> usize { + self.1.len() + } + + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + for map in self.0.iter().rev() { + if map.contains_key(name) { + return map.insert(name, value); + } + } + + panic!("New entries may not be added to MergedMapView") + } + + fn keys(&self) -> Vec { + self.1.iter().copied().collect() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + unimplemented!() + } +} + +#[derive(Debug, Clone)] +pub(crate) struct PublicMemberMapView + Clone>(pub T); + +impl + Clone> MapView for PublicMemberMapView { + type Value = V; + fn get(&self, name: Identifier) -> Option { + if !name.is_public() { + return None; + } + + self.0.get(name) + } + + fn remove(&self, name: Identifier) -> Option { + if !name.is_public() { + return None; + } + + self.0.remove(name) + } + + fn insert(&self, name: Identifier, value: Self::Value) -> Option { + if !name.is_public() { + return None; + } + + self.0.insert(name, value) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn keys(&self) -> Vec { + self.0 + .keys() + .iter() + .copied() + .filter(Identifier::is_public) + .collect() + } + + fn iter(&self) -> Vec<(Identifier, Self::Value)> { + unimplemented!() + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 94a47dee..caa302c1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,11 +1,97 @@ pub(crate) use chars::*; -pub(crate) use comment_whitespace::*; -pub(crate) use number::*; -pub(crate) use read_until::*; +pub(crate) use map_view::*; pub(crate) use strings::*; mod chars; -mod comment_whitespace; -mod number; -mod read_until; +mod map_view; mod strings; + +#[allow(clippy::case_sensitive_file_extension_comparisons)] +pub(crate) 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("//") +} + +pub(crate) fn opposite_bracket(b: char) -> char { + debug_assert!(matches!(b, '(' | '{' | '[' | ')' | '}' | ']')); + match b { + '(' => ')', + '{' => '}', + '[' => ']', + ')' => '(', + '}' => '{', + ']' => '[', + _ => unreachable!(), + } +} + +pub(crate) fn is_special_function(s: &str) -> bool { + s.starts_with("calc(") + || s.starts_with("var(") + || s.starts_with("env(") + || s.starts_with("min(") + || s.starts_with("max(") + || s.starts_with("clamp(") +} + +/// Trim ASCII whitespace from both sides of string. +/// +/// If [excludeEscape] is `true`, this doesn't trim whitespace included in a CSS +/// escape. +pub(crate) fn trim_ascii( + s: &str, + // default=false + exclude_escape: bool, +) -> &str { + match s.chars().position(|c| !c.is_ascii_whitespace()) { + Some(start) => &s[start..=last_non_whitespace(s, exclude_escape).unwrap()], + None => "", + } +} + +fn last_non_whitespace(s: &str, exclude_escape: bool) -> Option { + let mut idx = s.len() - 1; + for c in s.chars().rev() { + if !c.is_ascii_whitespace() { + if exclude_escape && idx != 0 && idx != s.len() - 1 && c == '\\' { + return Some(idx + 1); + } else { + return Some(idx); + } + } + + idx -= 1; + } + + None +} + +pub(crate) fn to_sentence>(mut elems: Vec, conjunction: &'static str) -> String { + debug_assert!( + !elems.is_empty(), + "expected sentence to contain at least one element" + ); + if elems.len() == 1 { + return elems.pop().unwrap().into(); + } + + let last = elems.pop().unwrap(); + + format!( + "{} {conjunction} {}", + elems + .into_iter() + .map(Into::into) + .collect::>() + .join(", "), + last.into() + ) +} diff --git a/src/utils/number.rs b/src/utils/number.rs deleted file mode 100644 index f3e8f7d4..00000000 --- a/src/utils/number.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[derive(Debug)] -pub(crate) struct ParsedNumber { - /// The full number excluding the decimal - /// - /// E.g. for `1.23`, this would be `"123"` - pub num: String, - - /// The length of the decimal - /// - /// E.g. for `1.23`, this would be `2` - pub dec_len: usize, - - /// The number following e in a scientific notated number - /// - /// E.g. for `1e23`, this would be `"23"`, - /// for `1`, this would be an empty string - // TODO: maybe we just return a bigint? - pub times_ten: String, - - /// Whether or not `times_ten` is negative - /// - /// E.g. for `1e-23` this would be `true`, - /// for `1e23` this would be `false` - pub times_ten_is_postive: bool, -} - -impl ParsedNumber { - pub const fn new( - num: String, - dec_len: usize, - times_ten: String, - times_ten_is_postive: bool, - ) -> Self { - Self { - num, - dec_len, - times_ten, - times_ten_is_postive, - } - } -} diff --git a/src/utils/read_until.rs b/src/utils/read_until.rs deleted file mode 100644 index 8e665f1c..00000000 --- a/src/utils/read_until.rs +++ /dev/null @@ -1,224 +0,0 @@ -use crate::{error::SassResult, lexer::Lexer, Token}; - -use super::{devour_whitespace, read_until_newline}; - -// Eat tokens until an open curly brace -// -// Does not consume the open curly brace -pub(crate) fn read_until_open_curly_brace(toks: &mut Lexer) -> SassResult> { - let mut t = Vec::new(); - let mut n = 0; - while let Some(tok) = toks.peek() { - match tok.kind { - '{' => n += 1, - '}' => n -= 1, - '/' => { - let next = toks.next().unwrap(); - match toks.peek() { - Some(Token { kind: '/', .. }) => read_until_newline(toks), - _ => t.push(next), - }; - continue; - } - '\\' => { - t.push(toks.next().unwrap()); - t.push(match toks.next() { - Some(tok) => tok, - None => continue, - }); - } - q @ '"' | q @ '\'' => { - t.push(toks.next().unwrap()); - t.extend(read_until_closing_quote(toks, q)?); - continue; - } - _ => {} - } - if n == 1 { - break; - } - - t.push(toks.next().unwrap()); - } - Ok(t) -} - -pub(crate) fn read_until_closing_curly_brace(toks: &mut Lexer) -> SassResult> { - let mut buf = Vec::new(); - let mut nesting = 0; - while let Some(tok) = toks.peek() { - match tok.kind { - q @ '"' | q @ '\'' => { - buf.push(toks.next().unwrap()); - buf.extend(read_until_closing_quote(toks, q)?); - } - '{' => { - nesting += 1; - buf.push(toks.next().unwrap()); - } - '}' => { - if nesting == 0 { - break; - } - - nesting -= 1; - buf.push(toks.next().unwrap()); - } - '/' => { - let next = toks.next().unwrap(); - match toks.peek() { - Some(Token { kind: '/', .. }) => { - read_until_newline(toks); - devour_whitespace(toks); - } - Some(..) | None => buf.push(next), - }; - continue; - } - '(' => { - buf.push(toks.next().unwrap()); - buf.extend(read_until_closing_paren(toks)?); - } - '\\' => { - buf.push(toks.next().unwrap()); - buf.push(match toks.next() { - Some(tok) => tok, - None => continue, - }); - } - _ => buf.push(toks.next().unwrap()), - } - } - devour_whitespace(toks); - Ok(buf) -} - -/// Read tokens into a vector until a matching closing quote is found -/// -/// The closing quote is included in the output -pub(crate) fn read_until_closing_quote(toks: &mut Lexer, q: char) -> SassResult> { - let mut t = Vec::new(); - while let Some(tok) = toks.next() { - match tok.kind { - '"' if q == '"' => { - t.push(tok); - break; - } - '\'' if q == '\'' => { - t.push(tok); - break; - } - '\\' => { - t.push(tok); - t.push(match toks.next() { - Some(tok) => tok, - None => return Err((format!("Expected {}.", q), tok.pos).into()), - }); - } - '#' => { - t.push(tok); - match toks.peek() { - Some(tok @ Token { kind: '{', .. }) => { - t.push(tok); - toks.next(); - t.append(&mut read_until_closing_curly_brace(toks)?); - } - Some(..) => continue, - None => return Err((format!("Expected {}.", q), tok.pos).into()), - } - } - _ => t.push(tok), - } - } - if let Some(tok) = t.pop() { - if tok.kind != q { - return Err((format!("Expected {}.", q), tok.pos).into()); - } - t.push(tok); - } - Ok(t) -} - -pub(crate) fn read_until_semicolon_or_closing_curly_brace( - toks: &mut Lexer, -) -> SassResult> { - let mut t = Vec::new(); - let mut nesting = 0; - while let Some(tok) = toks.peek() { - match tok.kind { - ';' => { - break; - } - '\\' => { - t.push(toks.next().unwrap()); - t.push(match toks.next() { - Some(tok) => tok, - None => continue, - }); - } - '"' | '\'' => { - let quote = toks.next().unwrap(); - t.push(quote); - t.extend(read_until_closing_quote(toks, quote.kind)?); - } - '{' => { - nesting += 1; - t.push(toks.next().unwrap()); - } - '}' => { - if nesting == 0 { - break; - } - - nesting -= 1; - t.push(toks.next().unwrap()); - } - '/' => { - let next = toks.next().unwrap(); - match toks.peek() { - Some(Token { kind: '/', .. }) => { - read_until_newline(toks); - devour_whitespace(toks); - } - _ => t.push(next), - }; - continue; - } - _ => t.push(toks.next().unwrap()), - } - } - devour_whitespace(toks); - Ok(t) -} - -pub(crate) fn read_until_closing_paren(toks: &mut Lexer) -> SassResult> { - let mut t = Vec::new(); - let mut scope = 0; - while let Some(tok) = toks.next() { - match tok.kind { - ')' => { - if scope < 1 { - t.push(tok); - return Ok(t); - } - - scope -= 1; - } - '(' => scope += 1, - '"' | '\'' => { - t.push(tok); - t.extend(read_until_closing_quote(toks, tok.kind)?); - continue; - } - '\\' => { - t.push(match toks.next() { - Some(tok) => tok, - None => continue, - }); - } - _ => {} - } - t.push(tok); - } - Ok(t) -} diff --git a/src/value/arglist.rs b/src/value/arglist.rs new file mode 100644 index 00000000..af0c75b7 --- /dev/null +++ b/src/value/arglist.rs @@ -0,0 +1,68 @@ +use std::{cell::Cell, collections::BTreeMap, sync::Arc}; + +use crate::common::{Identifier, ListSeparator}; + +use super::Value; + +#[derive(Debug, Clone)] +pub(crate) struct ArgList { + pub elems: Vec, + were_keywords_accessed: Arc>, + // todo: special wrapper around this field to avoid having to make it private? + keywords: BTreeMap, + pub separator: ListSeparator, +} + +impl PartialEq for ArgList { + fn eq(&self, other: &Self) -> bool { + self.elems == other.elems + && self.keywords == other.keywords + && self.separator == other.separator + } +} + +impl Eq for ArgList {} + +impl ArgList { + pub fn new( + elems: Vec, + were_keywords_accessed: Arc>, + keywords: BTreeMap, + separator: ListSeparator, + ) -> Self { + debug_assert!( + !(*were_keywords_accessed).get(), + "expected args to initialize with unaccessed keywords" + ); + + Self { + elems, + were_keywords_accessed, + keywords, + separator, + } + } + + pub fn len(&self) -> usize { + self.elems.len() + self.keywords.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn is_null(&self) -> bool { + // todo: include keywords + !self.is_empty() && (self.elems.iter().all(Value::is_null)) + } + + pub fn keywords(&self) -> &BTreeMap { + (*self.were_keywords_accessed).set(true); + &self.keywords + } + + pub fn into_keywords(self) -> BTreeMap { + (*self.were_keywords_accessed).set(true); + self.keywords + } +} diff --git a/src/value/calculation.rs b/src/value/calculation.rs new file mode 100644 index 00000000..9e78aedb --- /dev/null +++ b/src/value/calculation.rs @@ -0,0 +1,418 @@ +use core::fmt; +use std::iter::Iterator; + +use codemap::Span; + +use crate::{ + common::BinaryOp, + error::SassResult, + serializer::inspect_number, + unit::Unit, + value::{SassNumber, Value}, + Options, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum CalculationArg { + Number(SassNumber), + Calculation(SassCalculation), + String(String), + Operation { + lhs: Box, + op: BinaryOp, + rhs: Box, + }, + Interpolation(String), +} + +impl CalculationArg { + pub fn parenthesize_calculation_rhs(outer: BinaryOp, right: BinaryOp) -> bool { + if outer == BinaryOp::Div { + true + } else if outer == BinaryOp::Plus { + false + } else { + right == BinaryOp::Plus || right == BinaryOp::Minus + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum CalculationName { + Calc, + Min, + Max, + Clamp, +} + +impl fmt::Display for CalculationName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CalculationName::Calc => f.write_str("calc"), + CalculationName::Min => f.write_str("min"), + CalculationName::Max => f.write_str("max"), + CalculationName::Clamp => f.write_str("clamp"), + } + } +} + +impl CalculationName { + pub fn in_min_or_max(self) -> bool { + self == CalculationName::Min || self == CalculationName::Max + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SassCalculation { + pub name: CalculationName, + pub args: Vec, +} + +impl SassCalculation { + pub fn unsimplified(name: CalculationName, args: Vec) -> Self { + Self { name, args } + } + + pub fn calc(arg: CalculationArg) -> Value { + let arg = Self::simplify(arg); + match arg { + CalculationArg::Number(n) => Value::Dimension(n), + CalculationArg::Calculation(c) => Value::Calculation(c), + _ => Value::Calculation(SassCalculation { + name: CalculationName::Calc, + args: vec![arg], + }), + } + } + + pub fn min(args: Vec, options: &Options, span: Span) -> SassResult { + let args = Self::simplify_arguments(args); + debug_assert!(!args.is_empty(), "min() must have at least one argument."); + + let mut minimum: Option = None; + + for arg in &args { + match arg { + CalculationArg::Number(n) + if minimum.is_some() && !minimum.as_ref().unwrap().is_comparable_to(n) => + { + minimum = None; + break; + } + CalculationArg::Number(n) + if minimum.is_none() + || minimum.as_ref().unwrap().num() + > n.num().convert(&n.unit, &minimum.as_ref().unwrap().unit) => + { + minimum = Some(n.clone()); + } + CalculationArg::Number(..) => continue, + _ => { + minimum = None; + break; + } + } + } + + Ok(match minimum { + Some(min) => Value::Dimension(min), + None => { + Self::verify_compatible_numbers(&args, options, span)?; + + Value::Calculation(SassCalculation { + name: CalculationName::Min, + args, + }) + } + }) + } + + pub fn max(args: Vec, options: &Options, span: Span) -> SassResult { + let args = Self::simplify_arguments(args); + if args.is_empty() { + return Err(("max() must have at least one argument.", span).into()); + } + + let mut maximum: Option = None; + + for arg in &args { + match arg { + CalculationArg::Number(n) + if maximum.is_some() && !maximum.as_ref().unwrap().is_comparable_to(n) => + { + maximum = None; + break; + } + CalculationArg::Number(n) + if maximum.is_none() + || maximum.as_ref().unwrap().num() + < n.num().convert(&n.unit, &maximum.as_ref().unwrap().unit) => + { + maximum = Some(n.clone()); + } + CalculationArg::Number(..) => continue, + _ => { + maximum = None; + break; + } + } + } + + Ok(match maximum { + Some(max) => Value::Dimension(max), + None => { + Self::verify_compatible_numbers(&args, options, span)?; + + Value::Calculation(SassCalculation { + name: CalculationName::Max, + args, + }) + } + }) + } + + pub fn clamp( + min: CalculationArg, + value: Option, + max: Option, + options: &Options, + span: Span, + ) -> SassResult { + if value.is_none() && max.is_some() { + return Err(("If value is null, max must also be null.", span).into()); + } + + let min = Self::simplify(min); + let value = value.map(Self::simplify); + let max = max.map(Self::simplify); + + match (min.clone(), value.clone(), max.clone()) { + ( + CalculationArg::Number(min), + Some(CalculationArg::Number(value)), + Some(CalculationArg::Number(max)), + ) => { + if min.is_comparable_to(&value) && min.is_comparable_to(&max) { + if value.num <= min.num().convert(min.unit(), value.unit()) { + return Ok(Value::Dimension(min)); + } + + if value.num >= max.num().convert(max.unit(), value.unit()) { + return Ok(Value::Dimension(max)); + } + + return Ok(Value::Dimension(value)); + } + } + _ => {} + } + + let mut args = vec![min]; + + if let Some(value) = value { + args.push(value); + } + + if let Some(max) = max { + args.push(max); + } + + Self::verify_length(&args, 3, span)?; + Self::verify_compatible_numbers(&args, options, span)?; + + Ok(Value::Calculation(SassCalculation { + name: CalculationName::Clamp, + args, + })) + } + + fn verify_length(args: &[CalculationArg], len: usize, span: Span) -> SassResult<()> { + if args.len() == len { + return Ok(()); + } + + if args.iter().any(|arg| { + matches!( + arg, + CalculationArg::String(..) | CalculationArg::Interpolation(..) + ) + }) { + return Ok(()); + } + + let was_or_were = if args.len() == 1 { "was" } else { "were" }; + + Err(( + format!( + "{len} arguments required, but only {} {was_or_were} passed.", + args.len() + ), + span, + ) + .into()) + } + + #[allow(clippy::needless_range_loop)] + fn verify_compatible_numbers( + args: &[CalculationArg], + options: &Options, + span: Span, + ) -> SassResult<()> { + for arg in args { + match arg { + CalculationArg::Number(num) => match &num.unit { + Unit::Complex { numer, denom } => { + if numer.len() > 1 || !denom.is_empty() { + let num = num.clone(); + let value = Value::Dimension(num); + return Err(( + format!( + "Number {} isn't compatible with CSS calculations.", + value.to_css_string(span, false)? + ), + span, + ) + .into()); + } + } + _ => continue, + }, + _ => continue, + } + } + + for i in 0..args.len() { + let number1 = match &args[i] { + CalculationArg::Number(num) => num, + _ => continue, + }; + + for j in (i + 1)..args.len() { + let number2 = match &args[j] { + CalculationArg::Number(num) => num, + _ => continue, + }; + + if number1.has_possibly_compatible_units(number2) { + continue; + } + + return Err(( + format!( + "{} and {} are incompatible.", + inspect_number(number1, options, span)?, + inspect_number(number2, options, span)? + ), + span, + ) + .into()); + } + } + + Ok(()) + } + + pub fn operate_internal( + mut op: BinaryOp, + left: CalculationArg, + right: CalculationArg, + in_min_or_max: bool, + simplify: bool, + options: &Options, + span: Span, + ) -> SassResult { + if !simplify { + return Ok(CalculationArg::Operation { + lhs: Box::new(left), + op, + rhs: Box::new(right), + }); + } + + let left = Self::simplify(left); + let mut right = Self::simplify(right); + + if op == BinaryOp::Plus || op == BinaryOp::Minus { + match (&left, &right) { + (CalculationArg::Number(left), CalculationArg::Number(right)) + if if in_min_or_max { + left.is_comparable_to(right) + } else { + left.has_compatible_units(&right.unit) + } => + { + if op == BinaryOp::Plus { + return Ok(CalculationArg::Number(left.clone() + right.clone())); + } else { + return Ok(CalculationArg::Number(left.clone() - right.clone())); + } + } + _ => {} + } + + Self::verify_compatible_numbers(&[left.clone(), right.clone()], options, span)?; + + if let CalculationArg::Number(mut n) = right { + if n.num.is_negative() { + n.num.0 *= -1.0; + op = if op == BinaryOp::Plus { + BinaryOp::Minus + } else { + BinaryOp::Plus + } + } else { + } + right = CalculationArg::Number(n); + } + + return Ok(CalculationArg::Operation { + lhs: Box::new(left), + op, + rhs: Box::new(right), + }); + } + + match (left, right) { + (CalculationArg::Number(num1), CalculationArg::Number(num2)) => { + if op == BinaryOp::Mul { + Ok(CalculationArg::Number(num1 * num2)) + } else { + Ok(CalculationArg::Number(num1 / num2)) + } + } + (left, right) => Ok(CalculationArg::Operation { + lhs: Box::new(left), + op, + rhs: Box::new(right), + }), + } + + // _verifyCompatibleNumbers([left, right]); + + // Ok(CalculationArg::Operation { + // lhs: Box::new(left), + // op, + // rhs: Box::new(right), + // }) + } + + fn simplify(arg: CalculationArg) -> CalculationArg { + match arg { + CalculationArg::Number(..) + | CalculationArg::Operation { .. } + | CalculationArg::Interpolation(..) + | CalculationArg::String(..) => arg, + CalculationArg::Calculation(mut calc) => { + if calc.name == CalculationName::Calc { + calc.args.remove(0) + } else { + CalculationArg::Calculation(calc) + } + } + } + } + + fn simplify_arguments(args: Vec) -> Vec { + args.into_iter().map(Self::simplify).collect() + } +} diff --git a/src/value/css_function.rs b/src/value/css_function.rs deleted file mode 100644 index 671c0df6..00000000 --- a/src/value/css_function.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub(crate) fn is_special_function(s: &str) -> bool { - s.starts_with("calc(") - || s.starts_with("var(") - || s.starts_with("env(") - || s.starts_with("min(") - || s.starts_with("max(") - || s.starts_with("clamp(") -} diff --git a/src/value/map.rs b/src/value/map.rs index 37b7abe7..1e3ce509 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -1,12 +1,14 @@ use std::{slice::Iter, vec::IntoIter}; +use codemap::Spanned; + use crate::{ common::{Brackets, ListSeparator}, value::Value, }; #[derive(Debug, Clone, Default)] -pub(crate) struct SassMap(Vec<(Value, Value)>); +pub(crate) struct SassMap(Vec<(Spanned, Value)>); impl PartialEq for SassMap { fn eq(&self, other: &Self) -> bool { @@ -17,7 +19,7 @@ impl PartialEq for SassMap { if !other .0 .iter() - .any(|(key2, value2)| key == key2 && value == value2) + .any(|(key2, value2)| key.node == key2.node && value == value2) { return false; } @@ -33,17 +35,23 @@ impl SassMap { SassMap(Vec::new()) } - pub const fn new_with(elements: Vec<(Value, Value)>) -> SassMap { + pub const fn new_with(elements: Vec<(Spanned, Value)>) -> SassMap { SassMap(elements) } - /// We take by value here (consuming the map) in order to - /// save a clone of the value, since the only place this - /// should be called is in a builtin function, which throws - /// away the map immediately anyway pub fn get(self, key: &Value) -> Option { for (k, v) in self.0 { - if &k == key { + if &k.node == key { + return Some(v); + } + } + + None + } + + pub fn get_ref(&self, key: &Value) -> Option<&Value> { + for (k, v) in &self.0 { + if &k.node == key { return Some(v); } } @@ -61,12 +69,12 @@ impl SassMap { } } - pub fn iter(&self) -> Iter<(Value, Value)> { + pub fn iter(&self) -> Iter<(Spanned, Value)> { self.0.iter() } pub fn keys(self) -> Vec { - self.0.into_iter().map(|(k, ..)| k).collect() + self.0.into_iter().map(|(k, ..)| k.node).collect() } pub fn values(self) -> Vec { @@ -76,19 +84,14 @@ impl SassMap { pub fn as_list(self) -> Vec { self.0 .into_iter() - .map(|(k, v)| Value::List(vec![k, v], ListSeparator::Space, Brackets::None)) + .map(|(k, v)| Value::List(vec![k.node, v], ListSeparator::Space, Brackets::None)) .collect() } - #[allow(clippy::missing_const_for_fn)] - pub fn entries(self) -> Vec<(Value, Value)> { - self.0 - } - /// Returns true if the key already exists - pub fn insert(&mut self, key: Value, value: Value) -> bool { + pub fn insert(&mut self, key: Spanned, value: Value) -> bool { for (ref k, ref mut v) in &mut self.0 { - if k == &key { + if k.node == key.node { *v = value; return true; } @@ -96,10 +99,14 @@ impl SassMap { self.0.push((key, value)); false } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } impl IntoIterator for SassMap { - type Item = (Value, Value); + type Item = (Spanned, Value); type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { diff --git a/src/value/mod.rs b/src/value/mod.rs index 0162bdc6..7ff363c6 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,68 +1,84 @@ -use std::cmp::Ordering; +use std::{borrow::Cow, cmp::Ordering}; use codemap::{Span, Spanned}; use crate::{ color::Color, - common::{Brackets, ListSeparator, Op, QuoteKind}, + common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, error::SassResult, - lexer::Lexer, - parse::Parser, + evaluate::Visitor, selector::Selector, + serializer::{inspect_number, serialize_calculation, serialize_color, serialize_number}, unit::Unit, - utils::hex_char_for, - {Cow, Token}, + utils::{hex_char_for, is_special_function}, + Options, OutputStyle, }; -use css_function::is_special_function; +pub(crate) use arglist::ArgList; +pub(crate) use calculation::*; pub(crate) use map::SassMap; -pub(crate) use number::Number; -pub(crate) use sass_function::SassFunction; +pub(crate) use number::*; +pub(crate) use sass_function::{SassFunction, UserDefinedFunction}; +pub(crate) use sass_number::{conversion_factor, SassNumber}; -pub(crate) mod css_function; +mod arglist; +mod calculation; mod map; mod number; mod sass_function; +mod sass_number; #[derive(Debug, Clone)] pub(crate) enum Value { - Important, True, False, Null, - /// A `None` value for `Number` indicates a `NaN` value - Dimension(Option, Unit, bool), + Dimension(SassNumber), List(Vec, ListSeparator, Brackets), + // todo: benchmark unboxing this, now that it's smaller Color(Box), String(String, QuoteKind), Map(SassMap), - ArgList(Vec>), + ArgList(ArgList), /// Returned by `get-function()` + // todo: benchmark boxing this (function refs are infrequent) FunctionRef(SassFunction), + Calculation(SassCalculation), } impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match self { + Value::Calculation(calc1) => match other { + Value::Calculation(calc2) => calc1 == calc2, + _ => false, + }, Value::String(s1, ..) => match other { Value::String(s2, ..) => s1 == s2, _ => false, }, - Value::Dimension(Some(n), unit, _) => match other { - Value::Dimension(Some(n2), unit2, _) => { + Value::Dimension(SassNumber { + num: n, + unit, + as_slash: _, + }) => match other { + Value::Dimension(SassNumber { + num: n2, + unit: unit2, + as_slash: _, + }) => { if !unit.comparable(unit2) { - false - } else if unit == unit2 { - n == n2 - } else if unit == &Unit::None || unit2 == &Unit::None { - false - } else { - n == &n2.clone().convert(unit2, unit) + return false; + } + + if (*unit2 == Unit::None || *unit == Unit::None) && unit != unit2 { + return false; } + + *n == n2.convert(unit2, unit) } _ => false, }, - Value::Dimension(None, ..) => false, Value::List(list1, sep1, brackets1) => match other { Value::List(list2, sep2, brackets2) => { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { @@ -81,7 +97,6 @@ impl PartialEq for Value { Value::Null => matches!(other, Value::Null), Value::True => matches!(other, Value::True), Value::False => matches!(other, Value::False), - Value::Important => matches!(other, Value::Important), Value::FunctionRef(fn1) => { if let Value::FunctionRef(fn2) = other { fn1 == fn2 @@ -110,8 +125,8 @@ impl PartialEq for Value { return false; } - for (el1, el2) in list1.iter().zip(list2) { - if &el1.node != el2 { + for (el1, el2) in list1.elems.iter().zip(list2) { + if el1 != el2 { return false; } } @@ -193,45 +208,87 @@ fn visit_quoted_string(buf: &mut String, force_double_quote: bool, string: &str) } impl Value { + pub fn with_slash( + self, + numerator: SassNumber, + denom: SassNumber, + span: Span, + ) -> SassResult { + let mut number = self.assert_number(span)?; + number.as_slash = Some(Box::new((numerator, denom))); + Ok(Value::Dimension(number)) + } + + pub fn assert_number(self, span: Span) -> SassResult { + match self { + Value::Dimension(n) => Ok(n), + _ => Err((format!("{} is not a number.", self.inspect(span)?), span).into()), + } + } + + pub fn assert_number_with_name(self, name: &str, span: Span) -> SassResult { + match self { + Value::Dimension(n) => Ok(n), + _ => Err(( + format!("${name}: {} is not a number.", self.inspect(span)?), + span, + ) + .into()), + } + } + + pub fn assert_color_with_name(self, name: &str, span: Span) -> SassResult { + match self { + Value::Color(c) => Ok(*c), + _ => Err(( + format!("${name}: {} is not a color.", self.inspect(span)?), + span, + ) + .into()), + } + } + + // todo: rename is_blank pub fn is_null(&self) -> bool { match self { Value::Null => true, Value::String(i, QuoteKind::None) if i.is_empty() => true, - Value::List(v, _, Brackets::Bracketed) if v.is_empty() => false, + Value::List(_, _, Brackets::Bracketed) => false, Value::List(v, ..) => v.iter().map(Value::is_null).all(|f| f), - Value::ArgList(v, ..) if v.is_empty() => false, - Value::ArgList(v, ..) => v.iter().map(|v| v.node.is_null()).all(|f| f), + Value::ArgList(v, ..) => v.is_null(), + _ => false, + } + } + + pub fn is_empty_list(&self) -> bool { + match self { + Value::List(v, ..) => v.is_empty(), + Value::Map(m) => m.is_empty(), + Value::ArgList(v) => v.elems.is_empty(), _ => false, } } pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult> { Ok(match self { - Value::Important => Cow::const_str("!important"), - Value::Dimension(num, unit, _) => match unit { - Unit::Mul(..) | Unit::Div(..) => { - if let Some(num) = num { - return Err(( - format!( - "{}{} isn't a valid CSS value.", - num.to_string(is_compressed), - unit - ), - span, - ) - .into()); - } - - return Err((format!("NaN{} isn't a valid CSS value.", unit), span).into()); - } - _ => { - if let Some(num) = num { - Cow::owned(format!("{}{}", num.to_string(is_compressed), unit)) - } else { - Cow::owned(format!("NaN{}", unit)) - } - } - }, + Value::Calculation(calc) => Cow::Owned(serialize_calculation( + calc, + &Options::default().style(if is_compressed { + OutputStyle::Compressed + } else { + OutputStyle::Expanded + }), + span, + )?), + Value::Dimension(n) => Cow::Owned(serialize_number( + n, + &Options::default().style(if is_compressed { + OutputStyle::Compressed + } else { + OutputStyle::Expanded + }), + span, + )?), Value::Map(..) | Value::FunctionRef(..) => { return Err(( format!("{} isn't a valid CSS value.", self.inspect(span)?), @@ -240,7 +297,7 @@ impl Value { .into()) } Value::List(vals, sep, brackets) => match brackets { - Brackets::None => Cow::owned( + Brackets::None => Cow::Owned( vals.iter() .filter(|x| !x.is_null()) .map(|x| x.to_css_string(span, is_compressed)) @@ -251,7 +308,7 @@ impl Value { sep.as_str() }), ), - Brackets::Bracketed => Cow::owned(format!( + Brackets::Bracketed => Cow::Owned(format!( "[{}]", vals.iter() .filter(|x| !x.is_null()) @@ -264,7 +321,15 @@ impl Value { }), )), }, - Value::Color(c) => Cow::owned(c.to_string()), + Value::Color(c) => Cow::Owned(serialize_color( + c, + &Options::default().style(if is_compressed { + OutputStyle::Compressed + } else { + OutputStyle::Expanded + }), + span, + )), Value::String(string, QuoteKind::None) => { let mut after_newline = false; let mut buf = String::with_capacity(string.len()); @@ -285,23 +350,24 @@ impl Value { } } } - Cow::owned(buf) + Cow::Owned(buf) } Value::String(string, QuoteKind::Quoted) => { let mut buf = String::with_capacity(string.len()); visit_quoted_string(&mut buf, false, string); - Cow::owned(buf) + Cow::Owned(buf) } - Value::True => Cow::const_str("true"), - Value::False => Cow::const_str("false"), - Value::Null => Cow::const_str(""), + Value::True => Cow::Borrowed("true"), + Value::False => Cow::Borrowed("false"), + Value::Null => Cow::Borrowed(""), Value::ArgList(args) if args.is_empty() => { return Err(("() isn't a valid CSS value.", span).into()); } - Value::ArgList(args) => Cow::owned( - args.iter() + Value::ArgList(args) => Cow::Owned( + args.elems + .iter() .filter(|x| !x.is_null()) - .map(|a| a.node.to_css_string(span, is_compressed)) + .map(|a| a.to_css_string(span, is_compressed)) .collect::>>>()? .join(if is_compressed { ListSeparator::Comma.as_compressed_str() @@ -333,7 +399,8 @@ impl Value { pub fn kind(&self) -> &'static str { match self { Value::Color(..) => "color", - Value::String(..) | Value::Important => "string", + Value::String(..) => "string", + Value::Calculation(..) => "calculation", Value::Dimension(..) => "number", Value::List(..) => "list", Value::FunctionRef(..) => "function", @@ -344,13 +411,46 @@ impl Value { } } - pub fn is_color(&self) -> bool { - matches!(self, Value::Color(..)) + pub fn as_slash(&self) -> Option> { + match self { + Value::Dimension(SassNumber { as_slash, .. }) => as_slash.clone(), + _ => None, + } + } + + pub fn without_slash(self) -> Self { + match self { + Value::Dimension(SassNumber { + num, + unit, + as_slash: _, + }) => Value::Dimension(SassNumber { + num, + unit, + as_slash: None, + }), + _ => self, + } } pub fn is_special_function(&self) -> bool { match self { Value::String(s, QuoteKind::None) => is_special_function(s), + Value::Calculation(..) => true, + _ => false, + } + } + + pub fn is_var(&self) -> bool { + match self { + Value::String(s, QuoteKind::None) => { + if s.len() < "var(--_)".len() { + return false; + } + + s.starts_with("var(") + } + Value::Calculation(..) => true, _ => false, } } @@ -363,21 +463,27 @@ impl Value { } } - pub fn cmp(&self, other: &Self, span: Span, op: Op) -> SassResult { + pub fn cmp(&self, other: &Self, span: Span, op: BinaryOp) -> SassResult> { Ok(match self { - Value::Dimension(None, ..) => todo!(), - Value::Dimension(Some(num), unit, _) => match &other { - Value::Dimension(None, ..) => todo!(), - Value::Dimension(Some(num2), unit2, _) => { + Value::Dimension(SassNumber { + num, + unit, + as_slash: _, + }) => match &other { + 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 || unit == &Unit::None || unit2 == &Unit::None { - num.cmp(num2) + num.partial_cmp(num2) } else { - num.cmp(&num2.clone().convert(unit2, unit)) + num.partial_cmp(&num2.convert(unit2, unit)) } } _ => { @@ -414,8 +520,16 @@ impl Value { Value::String(s2, ..) => s1 != s2, _ => true, }, - Value::Dimension(Some(n), unit, _) => match other { - Value::Dimension(Some(n2), unit2, _) => { + Value::Dimension(SassNumber { + num: n, + unit, + as_slash: _, + }) if !n.is_nan() => match other { + Value::Dimension(SassNumber { + num: n2, + unit: unit2, + as_slash: _, + }) if !n2.is_nan() => { if !unit.comparable(unit2) { true } else if unit == unit2 { @@ -423,7 +537,7 @@ impl Value { } else if unit == &Unit::None || unit2 == &Unit::None { true } else { - n != &n2.clone().convert(unit2, unit) + n != &n2.convert(unit2, unit) } } _ => true, @@ -449,23 +563,31 @@ impl Value { // TODO: // https://github.com/sass/dart-sass/blob/d4adea7569832f10e3a26d0e420ae51640740cfb/lib/src/ast/sass/expression/list.dart#L39 + // todo: is this actually fallible? pub fn inspect(&self, span: Span) -> SassResult> { Ok(match self { + Value::Calculation(calc) => { + Cow::Owned(serialize_calculation(calc, &Options::default(), span)?) + } Value::List(v, _, brackets) if v.is_empty() => match brackets { - Brackets::None => Cow::const_str("()"), - Brackets::Bracketed => Cow::const_str("[]"), + Brackets::None => Cow::Borrowed("()"), + Brackets::Bracketed => Cow::Borrowed("[]"), }, Value::List(v, sep, brackets) if v.len() == 1 => match brackets { Brackets::None => match sep { - ListSeparator::Space => v[0].inspect(span)?, - ListSeparator::Comma => Cow::owned(format!("({},)", v[0].inspect(span)?)), + ListSeparator::Space | ListSeparator::Slash | ListSeparator::Undecided => { + v[0].inspect(span)? + } + ListSeparator::Comma => Cow::Owned(format!("({},)", v[0].inspect(span)?)), }, Brackets::Bracketed => match sep { - ListSeparator::Space => Cow::owned(format!("[{}]", v[0].inspect(span)?)), - ListSeparator::Comma => Cow::owned(format!("[{},]", v[0].inspect(span)?)), + ListSeparator::Space | ListSeparator::Slash | ListSeparator::Undecided => { + Cow::Owned(format!("[{}]", v[0].inspect(span)?)) + } + ListSeparator::Comma => Cow::Owned(format!("[{},]", v[0].inspect(span)?)), }, }, - Value::List(vals, sep, brackets) => Cow::owned(match brackets { + Value::List(vals, sep, brackets) => Cow::Owned(match brackets { Brackets::None => vals .iter() .map(|x| x.inspect(span)) @@ -479,40 +601,37 @@ impl Value { .join(sep.as_str()), ), }), - Value::FunctionRef(f) => Cow::owned(format!("get-function(\"{}\")", f.name())), - Value::Null => Cow::const_str("null"), - Value::Map(map) => Cow::owned(format!( + Value::FunctionRef(f) => Cow::Owned(format!("get-function(\"{}\")", f.name())), + Value::Null => Cow::Borrowed("null"), + Value::Map(map) => Cow::Owned(format!( "({})", map.iter() .map(|(k, v)| Ok(format!("{}: {}", k.inspect(span)?, v.inspect(span)?))) .collect::>>()? .join(", ") )), - Value::Dimension(Some(num), unit, _) => { - Cow::owned(format!("{}{}", num.inspect(), unit)) - } - Value::Dimension(None, unit, ..) => Cow::owned(format!("NaN{}", unit)), - Value::ArgList(args) if args.is_empty() => Cow::const_str("()"), - Value::ArgList(args) if args.len() == 1 => Cow::owned(format!( + Value::Dimension(n) => Cow::Owned(inspect_number(n, &Options::default(), span)?), + Value::ArgList(args) if args.is_empty() => Cow::Borrowed("()"), + Value::ArgList(args) if args.len() == 1 => Cow::Owned(format!( "({},)", - args.iter() + args.elems + .iter() .filter(|x| !x.is_null()) - .map(|a| a.node.inspect(span)) + .map(|a| a.inspect(span)) .collect::>>>()? .join(", "), )), - Value::ArgList(args) => Cow::owned( - args.iter() + Value::ArgList(args) => Cow::Owned( + args.elems + .iter() .filter(|x| !x.is_null()) - .map(|a| a.node.inspect(span)) + .map(|a| a.inspect(span)) .collect::>>>()? .join(", "), ), - Value::Important - | Value::True - | Value::False - | Value::Color(..) - | Value::String(..) => self.to_css_string(span, false)?, + Value::True | Value::False | Value::Color(..) | Value::String(..) => { + self.to_css_string(span, false)? + } }) } @@ -520,11 +639,19 @@ impl Value { match self { Value::List(v, ..) => v, Value::Map(m) => m.as_list(), - Value::ArgList(v) => v.into_iter().map(|val| val.node).collect(), + Value::ArgList(v) => v.elems, v => vec![v], } } + pub fn separator(&self) -> ListSeparator { + match self { + Value::List(_, list_separator, _) => *list_separator, + Value::Map(..) | Value::ArgList(..) => ListSeparator::Comma, + _ => ListSeparator::Space, + } + } + /// Parses `self` as a selector list, in the same manner as the /// `selector-parse()` function. /// @@ -535,39 +662,21 @@ impl Value { /// `name` is the argument name. It's used for error reporting. pub fn to_selector( self, - parser: &mut Parser, + visitor: &mut Visitor, name: &str, allows_parent: bool, + span: Span, ) -> SassResult { - let string = match self.clone().selector_string(parser.span_before)? { + let string = match self.clone().selector_string(span)? { Some(v) => v, - None => return Err((format!("${}: {} is not a valid selector: it must be a string, a list of strings, or a list of lists of strings.", name, self.inspect(parser.span_before)?), parser.span_before).into()), + None => return Err((format!("${}: {} is not a valid selector: it must be a string,\n a list of strings, or a list of lists of strings.", name, self.inspect(span)?), span).into()), }; - Ok(Parser { - toks: &mut Lexer::new( - string - .chars() - .map(|c| Token::new(parser.span_before, c)) - .collect::>(), - ), - map: parser.map, - path: parser.path, - scopes: parser.scopes, - global_scope: parser.global_scope, - super_selectors: parser.super_selectors, - span_before: parser.span_before, - content: parser.content, - flags: parser.flags, - at_root: parser.at_root, - at_root_has_selector: parser.at_root_has_selector, - extender: parser.extender, - content_scopes: parser.content_scopes, - options: parser.options, - modules: parser.modules, - module_config: parser.module_config, - } - .parse_selector(allows_parent, true, String::new())? - .0) + Ok(Selector(visitor.parse_selector_from_string( + &string, + allows_parent, + true, + span, + )?)) } #[allow(clippy::only_used_in_recursion)] @@ -581,7 +690,12 @@ impl Value { for complex in list { if let Value::String(text, ..) = complex { result.push(text); - } else if let Value::List(_, ListSeparator::Space, ..) = complex { + } else if let Value::List( + _, + ListSeparator::Space | ListSeparator::Undecided, + .., + ) = complex + { result.push(match complex.selector_string(span)? { Some(v) => v, None => return Ok(None), @@ -591,7 +705,8 @@ impl Value { } } } - ListSeparator::Space => { + ListSeparator::Slash => return Ok(None), + ListSeparator::Space | ListSeparator::Undecided => { for compound in list { if let Value::String(text, ..) = compound { result.push(text); @@ -608,7 +723,68 @@ impl Value { })) } - pub fn is_quoted_string(&self) -> bool { - matches!(self, Value::String(_, QuoteKind::Quoted)) + pub fn unary_plus(self, visitor: &mut Visitor, span: Span) -> SassResult { + Ok(match self { + Self::Dimension(SassNumber { .. }) => self, + Self::Calculation(..) => { + return Err(( + format!("Undefined operation \"+{}\".", self.inspect(span)?), + span, + ) + .into()) + } + _ => Self::String( + format!( + "+{}", + &self.to_css_string(span, visitor.options.is_compressed())? + ), + QuoteKind::None, + ), + }) + } + + pub fn unary_neg(self, visitor: &mut Visitor, span: Span) -> SassResult { + Ok(match self { + Self::Calculation(..) => { + return Err(( + format!("Undefined operation \"-{}\".", self.inspect(span)?), + span, + ) + .into()) + } + Self::Dimension(SassNumber { + num, + unit, + as_slash, + }) => Self::Dimension(SassNumber { + num: -num, + unit, + as_slash, + }), + _ => Self::String( + format!( + "-{}", + &self.to_css_string(span, visitor.options.is_compressed())? + ), + QuoteKind::None, + ), + }) + } + + pub fn unary_div(self, visitor: &mut Visitor, span: Span) -> SassResult { + Ok(Self::String( + format!( + "/{}", + &self.to_css_string(span, visitor.options.is_compressed())? + ), + QuoteKind::None, + )) + } + + pub fn unary_not(self) -> Self { + match self { + Self::False | Self::Null => Self::True, + _ => Self::False, + } } } diff --git a/src/value/number.rs b/src/value/number.rs new file mode 100644 index 00000000..1784a75e --- /dev/null +++ b/src/value/number.rs @@ -0,0 +1,440 @@ +use std::{ + convert::From, + fmt, mem, + ops::{ + Add, AddAssign, Deref, DerefMut, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, + SubAssign, + }, +}; + +use crate::{ + error::SassResult, + unit::{Unit, UNIT_CONVERSION_TABLE}, +}; + +use codemap::Span; + +const PRECISION: i32 = 10; + +fn epsilon() -> f64 { + 10.0_f64.powi(-PRECISION - 1) +} + +fn inverse_epsilon() -> f64 { + 10.0_f64.powi(PRECISION + 1) +} + +/// Thin wrapper around `f64` providing utility functions and more accurate +/// operations -- namely a Sass-compatible modulo +// todo: potentially superfluous? +#[derive(Clone, Copy, PartialOrd)] +#[repr(transparent)] +pub(crate) struct Number(pub f64); + +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + fuzzy_equals(self.0, other.0) + } +} + +impl Eq for Number {} + +pub(crate) fn fuzzy_equals(a: f64, b: f64) -> bool { + if a == b { + return true; + } + + (a - b).abs() <= epsilon() && (a * inverse_epsilon()).round() == (b * inverse_epsilon()).round() +} + +pub(crate) fn fuzzy_as_int(num: f64) -> Option { + if !num.is_finite() { + return None; + } + + let rounded = num.round(); + + if fuzzy_equals(num, rounded) { + Some(rounded as i32) + } else { + None + } +} + +pub(crate) fn fuzzy_round(number: f64) -> f64 { + // If the number is within epsilon of X.5, round up (or down for negative + // numbers). + if number > 0.0 { + if fuzzy_less_than(number % 1.0, 0.5) { + number.floor() + } else { + number.ceil() + } + } else if fuzzy_less_than_or_equals(number % 1.0, 0.5) { + number.floor() + } else { + number.ceil() + } +} + +pub(crate) fn fuzzy_less_than(number1: f64, number2: f64) -> bool { + number1 < number2 && !fuzzy_equals(number1, number2) +} + +pub(crate) fn fuzzy_less_than_or_equals(number1: f64, number2: f64) -> bool { + number1 < number2 || fuzzy_equals(number1, number2) +} + +impl Number { + pub fn min(self, other: Self) -> Self { + if self < other { + self + } else { + other + } + } + + pub fn max(self, other: Self) -> Self { + if self > other { + self + } else { + other + } + } + + pub fn is_positive(self) -> bool { + self.0.is_sign_positive() && !self.is_zero() + } + + pub fn is_negative(self) -> bool { + self.0.is_sign_negative() && !self.is_zero() + } + + pub fn assert_int(self, span: Span) -> SassResult { + match fuzzy_as_int(self.0) { + Some(i) => Ok(i), + None => Err((format!("{} is not an int.", self.0), span).into()), + } + } + + pub fn assert_int_with_name(self, name: &'static str, span: Span) -> SassResult { + match fuzzy_as_int(self.0) { + Some(i) => Ok(i), + None => Err(( + format!("${name}: {} is not an int.", self.to_string(false)), + span, + ) + .into()), + } + } + + pub fn round(self) -> Self { + Self(self.0.round()) + } + + pub fn ceil(self) -> Self { + Self(self.0.ceil()) + } + + pub fn floor(self) -> Self { + Self(self.0.floor()) + } + + pub fn abs(self) -> Self { + Self(self.0.abs()) + } + + pub fn is_decimal(self) -> bool { + self.0.fract() != 0.0 + } + + pub fn clamp(self, min: f64, max: f64) -> Self { + Number(min.max(self.0.min(max))) + } + + pub fn sqrt(self) -> Self { + Self(self.0.sqrt()) + } + + pub fn ln(self) -> Self { + Self(self.0.ln()) + } + + pub fn log(self, base: Number) -> Self { + Self(self.0.log(base.0)) + } + + pub fn pow(self, exponent: Self) -> Self { + Self(self.0.powf(exponent.0)) + } + + /// Invariants: `from.comparable(&to)` must be true + pub fn convert(self, from: &Unit, to: &Unit) -> Self { + if from == &Unit::None || to == &Unit::None || from == to { + return self; + } + + debug_assert!(from.comparable(to), "from: {:?}, to: {:?}", from, to); + + Number(self.0 * UNIT_CONVERSION_TABLE[to][from]) + } +} + +macro_rules! inverse_trig_fn( + ($name:ident) => { + pub fn $name(self) -> Self { + Self(self.0.$name().to_degrees()) + } + } +); + +/// Trigonometry methods +impl Number { + inverse_trig_fn!(acos); + inverse_trig_fn!(asin); + inverse_trig_fn!(atan); +} + +impl Default for Number { + fn default() -> Self { + Self::zero() + } +} + +impl Number { + pub const fn one() -> Self { + Self(1.0) + } + + pub fn is_one(self) -> bool { + fuzzy_equals(self.0, 1.0) + } + + pub const fn zero() -> Self { + Self(0.0) + } + + pub fn is_zero(self) -> bool { + fuzzy_equals(self.0, 0.0) + } +} + +impl Deref for Number { + type Target = f64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Number { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +macro_rules! from_integer { + ($ty:ty) => { + impl From<$ty> for Number { + fn from(b: $ty) -> Self { + Number(b as f64) + } + } + }; +} + +macro_rules! from_smaller_integer { + ($ty:ty) => { + impl From<$ty> for Number { + fn from(val: $ty) -> Self { + Self(f64::from(val)) + } + } + }; +} + +impl From for Number { + fn from(val: i64) -> Self { + Self(val as f64) + } +} + +impl From for Number { + fn from(b: f64) -> Self { + Self(b) + } +} + +from_integer!(usize); +from_integer!(isize); +from_smaller_integer!(i32); +from_smaller_integer!(u32); +from_smaller_integer!(u8); + +impl fmt::Debug for Number { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Number( {} )", self.to_string(false)) + } +} + +impl Number { + pub(crate) fn inspect(self) -> String { + self.to_string(false) + } + + pub(crate) fn to_string(self, is_compressed: bool) -> String { + if self.0.is_infinite() && self.0.is_sign_negative() { + return "-Infinity".to_owned(); + } else if self.0.is_infinite() { + return "Infinity".to_owned(); + } + + let mut buffer = String::with_capacity(3); + + if self.0 < 0.0 { + buffer.push('-'); + } + + let num = self.0.abs(); + + if is_compressed && num < 1.0 { + buffer.push_str( + format!("{:.10}", num)[1..] + .trim_end_matches('0') + .trim_end_matches('.'), + ); + } else { + buffer.push_str( + format!("{:.10}", num) + .trim_end_matches('0') + .trim_end_matches('.'), + ); + } + + if buffer.is_empty() || buffer == "-" || buffer == "-0" { + return "0".to_owned(); + } + + buffer + } +} + +impl Add for Number { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +impl AddAssign for Number { + fn add_assign(&mut self, other: Self) { + let tmp = mem::take(self); + *self = tmp + other; + } +} + +impl Sub for Number { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Self(self.0 - other.0) + } +} + +impl SubAssign for Number { + fn sub_assign(&mut self, other: Self) { + let tmp = mem::take(self); + *self = tmp - other; + } +} + +impl Mul for Number { + type Output = Self; + + fn mul(self, other: Self) -> Self { + Self(self.0 * other.0) + } +} + +impl Mul for Number { + type Output = Self; + + fn mul(self, other: i64) -> Self { + Self(self.0 * other as f64) + } +} + +impl MulAssign for Number { + fn mul_assign(&mut self, other: i64) { + let tmp = mem::take(self); + *self = tmp * other; + } +} + +impl MulAssign for Number { + fn mul_assign(&mut self, other: Self) { + let tmp = mem::take(self); + *self = tmp * other; + } +} + +impl Div for Number { + type Output = Self; + + fn div(self, other: Self) -> Self { + Self(self.0 / other.0) + } +} + +impl DivAssign for Number { + fn div_assign(&mut self, other: Self) { + let tmp = mem::take(self); + *self = tmp / other; + } +} + +fn real_mod(n1: f64, n2: f64) -> f64 { + n1.rem_euclid(n2) +} + +fn modulo(n1: f64, n2: f64) -> f64 { + if n2 > 0.0 { + return real_mod(n1, n2); + } + + if n2 == 0.0 { + return f64::NAN; + } + + let result = real_mod(n1, n2); + + if result == 0.0 { + 0.0 + } else { + result + n2 + } +} + +impl Rem for Number { + type Output = Self; + + fn rem(self, other: Self) -> Self { + Self(modulo(self.0, other.0)) + } +} + +impl RemAssign for Number { + fn rem_assign(&mut self, other: Self) { + let tmp = mem::take(self); + *self = tmp % other; + } +} + +impl Neg for Number { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} diff --git a/src/value/number/integer.rs b/src/value/number/integer.rs deleted file mode 100644 index 03f684b4..00000000 --- a/src/value/number/integer.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::{ - fmt::{self, Display, UpperHex}, - mem, - ops::{Add, AddAssign}, -}; - -use num_bigint::BigInt; -use num_traits::{Signed, ToPrimitive, Zero}; - -pub(crate) enum Integer { - Small(i64), - Big(BigInt), -} - -impl Integer { - pub fn abs(&self) -> Self { - match self { - Self::Small(v) => Self::Small(v.abs()), - Self::Big(v) => Self::Big(v.abs()), - } - } - - pub fn is_ten(&self) -> bool { - match self { - Self::Small(10) => true, - Self::Small(..) => false, - Self::Big(v) => v == &BigInt::from(10), - } - } -} - -impl Default for Integer { - fn default() -> Self { - Self::zero() - } -} - -impl UpperHex for Integer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Small(v) => write!(f, "{:02X}", v), - Self::Big(v) => write!(f, "{:02X}", v), - } - } -} - -impl Add for Integer { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - match self { - Self::Small(val1) => match rhs { - Self::Small(val2) => match val1.checked_add(val2) { - Some(v) => Self::Small(v), - None => Self::Big(BigInt::from(val1) + val2), - }, - Self::Big(val2) => Self::Big(BigInt::from(val1) + val2), - }, - Self::Big(val1) => match rhs { - Self::Big(val2) => Self::Big(val1 + val2), - Self::Small(val2) => Self::Big(val1 + BigInt::from(val2)), - }, - } - } -} - -impl Add for Integer { - type Output = Self; - fn add(self, rhs: BigInt) -> Self::Output { - match self { - Self::Small(v) => Self::Big(BigInt::from(v) + rhs), - Self::Big(v) => Self::Big(v + rhs), - } - } -} - -impl Add for Integer { - type Output = Self; - fn add(self, rhs: i32) -> Self::Output { - match self { - Self::Small(v) => match v.checked_add(i64::from(rhs)) { - Some(v) => Self::Small(v), - None => Self::Big(BigInt::from(v) + rhs), - }, - Self::Big(v) => Self::Big(v + rhs), - } - } -} - -impl AddAssign for Integer { - fn add_assign(&mut self, rhs: i32) { - let tmp = mem::take(self); - *self = tmp + rhs; - } -} - -impl AddAssign for Integer { - fn add_assign(&mut self, rhs: Self) { - let tmp = mem::take(self); - *self = tmp + rhs; - } -} - -impl Zero for Integer { - fn zero() -> Self { - Self::Small(0) - } - - fn is_zero(&self) -> bool { - match self { - Self::Small(0) => true, - Self::Small(..) => false, - Self::Big(v) => v.is_zero(), - } - } -} - -impl Display for Integer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Small(v) => write!(f, "{}", v), - Self::Big(v) => write!(f, "{}", v), - } - } -} - -impl ToPrimitive for Integer { - fn to_u8(&self) -> Option { - match self { - Self::Small(v) => v.to_u8(), - Self::Big(v) => v.to_u8(), - } - } - - fn to_u64(&self) -> Option { - match self { - Self::Small(v) => v.to_u64(), - Self::Big(v) => v.to_u64(), - } - } - - fn to_i64(&self) -> std::option::Option { - match self { - Self::Small(v) => Some(*v), - Self::Big(v) => v.to_i64(), - } - } -} diff --git a/src/value/number/mod.rs b/src/value/number/mod.rs deleted file mode 100644 index 10d2e1b6..00000000 --- a/src/value/number/mod.rs +++ /dev/null @@ -1,828 +0,0 @@ -use std::{ - cmp::Ordering, - convert::{From, TryFrom}, - fmt, mem, - ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign}, -}; - -use num_bigint::BigInt; -use num_rational::{BigRational, Rational64}; -use num_traits::{ - CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Num, One, Signed, ToPrimitive, Zero, -}; - -use crate::unit::{Unit, UNIT_CONVERSION_TABLE}; - -use integer::Integer; - -mod integer; - -const PRECISION: usize = 10; - -#[derive(Clone)] -pub(crate) enum Number { - Small(Rational64), - Big(Box), -} - -impl PartialEq for Number { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Number::Small(val1), Number::Small(val2)) => val1 == val2, - (Number::Big(val1), val2 @ Number::Small(..)) => { - **val1 == val2.clone().into_big_rational() - } - (val1 @ Number::Small(..), Number::Big(val2)) => { - val1.clone().into_big_rational() == **val2 - } - (Number::Big(val1), Number::Big(val2)) => val1 == val2, - } - } -} - -impl Eq for Number {} - -impl Number { - pub const fn new_small(val: Rational64) -> Number { - Number::Small(val) - } - - pub fn new_big(val: BigRational) -> Number { - Number::Big(Box::new(val)) - } - - fn into_big_rational(self) -> BigRational { - match self { - Number::Small(small) => { - let tuple: (i64, i64) = small.into(); - - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) - } - Number::Big(big) => *big, - } - } - - pub fn to_integer(&self) -> Integer { - match self { - Self::Small(val) => Integer::Small(val.to_integer()), - Self::Big(val) => Integer::Big(val.to_integer()), - } - } - - pub fn small_ratio, B: Into>(a: A, b: B) -> Self { - Number::new_small(Rational64::new(a.into(), b.into())) - } - - #[allow(dead_code)] - pub fn big_ratio, B: Into>(a: A, b: B) -> Self { - Number::new_big(BigRational::new(a.into(), b.into())) - } - - pub fn round(&self) -> Self { - match self { - Self::Small(val) => Self::Small(val.round()), - Self::Big(val) => Self::Big(Box::new(val.round())), - } - } - - pub fn ceil(&self) -> Self { - match self { - Self::Small(val) => Self::Small(val.ceil()), - Self::Big(val) => Self::Big(Box::new(val.ceil())), - } - } - - pub fn floor(&self) -> Self { - match self { - Self::Small(val) => Self::Small(val.floor()), - Self::Big(val) => Self::Big(Box::new(val.floor())), - } - } - - pub fn abs(&self) -> Self { - match self { - Self::Small(val) => Self::Small(val.abs()), - Self::Big(val) => Self::Big(Box::new(val.abs())), - } - } - - pub fn is_decimal(&self) -> bool { - match self { - Self::Small(v) => !v.is_integer(), - Self::Big(v) => !v.is_integer(), - } - } - - pub fn fract(&mut self) -> Number { - match self { - Self::Small(v) => Number::new_small(v.fract()), - Self::Big(v) => Number::new_big(v.fract()), - } - } - - pub fn clamp + Zero, B: Into>(self, min: A, max: B) -> Self { - let max = max.into(); - if self > max { - return max; - } - - if min.is_zero() && self.is_negative() { - return Number::zero(); - } - - let min = min.into(); - if self < min { - return min; - } - - self - } - - #[allow(clippy::cast_precision_loss)] - pub fn as_float(self) -> Option { - Some(match self { - Number::Small(n) => (*n.numer() as f64) / (*n.denom() as f64), - Number::Big(n) => (n.numer().to_f64()?) / (n.denom().to_f64()?), - }) - } - - pub fn sqrt(self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.sqrt(), - )?))) - } - - pub fn ln(self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.ln(), - )?))) - } - - pub fn log(self, base: Number) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.log(base.as_float()?), - )?))) - } - - pub fn pow(self, exponent: Self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.powf(exponent.as_float()?), - )?))) - } - - pub fn pi() -> Self { - Number::from(std::f64::consts::PI) - } - - pub fn atan2(self, other: Self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.atan2(other.as_float()?), - )?))) - } - - /// Invariants: `from.comparable(&to)` must be true - pub fn convert(self, from: &Unit, to: &Unit) -> Self { - debug_assert!(from.comparable(to)); - self * UNIT_CONVERSION_TABLE[to][from].clone() - } -} - -macro_rules! trig_fn( - ($name:ident, $name_deg:ident) => { - pub fn $name(self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.$name(), - )?))) - } - - pub fn $name_deg(self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.to_radians().$name(), - )?))) - } - } -); - -macro_rules! inverse_trig_fn( - ($name:ident) => { - pub fn $name(self) -> Option { - Some(Number::Big(Box::new(BigRational::from_float( - self.as_float()?.$name().to_degrees(), - )?))) - } - } -); - -/// Trigonometry methods -impl Number { - trig_fn!(cos, cos_deg); - trig_fn!(sin, sin_deg); - trig_fn!(tan, tan_deg); - - inverse_trig_fn!(acos); - inverse_trig_fn!(asin); - inverse_trig_fn!(atan); -} - -impl Default for Number { - fn default() -> Self { - Self::zero() - } -} - -impl Zero for Number { - fn zero() -> Self { - Number::new_small(Rational64::from_integer(0)) - } - - fn is_zero(&self) -> bool { - match self { - Self::Small(v) => v.is_zero(), - Self::Big(v) => v.is_zero(), - } - } -} - -impl One for Number { - fn one() -> Self { - Number::new_small(Rational64::from_integer(1)) - } - - fn is_one(&self) -> bool { - match self { - Self::Small(v) => v.is_one(), - Self::Big(v) => v.is_one(), - } - } -} - -impl Num for Number { - type FromStrRadixErr = (); - #[cold] - fn from_str_radix(_: &str, _: u32) -> Result { - unimplemented!() - } -} - -impl Signed for Number { - fn abs(&self) -> Self { - self.abs() - } - - #[cold] - fn abs_sub(&self, _: &Self) -> Self { - unimplemented!() - } - - #[cold] - fn signum(&self) -> Self { - if self.is_zero() { - Self::zero() - } else if self.is_positive() { - Self::one() - } else { - -Self::one() - } - } - - fn is_positive(&self) -> bool { - match self { - Self::Small(v) => v.is_positive(), - Self::Big(v) => v.is_positive(), - } - } - - fn is_negative(&self) -> bool { - match self { - Self::Small(v) => v.is_negative(), - Self::Big(v) => v.is_negative(), - } - } -} - -macro_rules! from_integer { - ($ty:ty) => { - impl From<$ty> for Number { - fn from(b: $ty) -> Self { - if let Ok(v) = i64::try_from(b) { - Number::Small(Rational64::from_integer(v)) - } else { - Number::Big(Box::new(BigRational::from_integer(BigInt::from(b)))) - } - } - } - }; -} - -macro_rules! from_smaller_integer { - ($ty:ty) => { - impl From<$ty> for Number { - fn from(val: $ty) -> Self { - Number::new_small(Rational64::from_integer(val as i64)) - } - } - }; -} - -impl From for Number { - fn from(val: i64) -> Self { - Number::new_small(Rational64::from_integer(val)) - } -} - -#[allow(clippy::fallible_impl_from)] -impl From for Number { - fn from(b: f64) -> Self { - Number::Big(Box::new(BigRational::from_float(b).unwrap())) - } -} - -from_integer!(usize); -from_integer!(isize); -from_smaller_integer!(i32); -from_smaller_integer!(u32); -from_smaller_integer!(u8); - -impl fmt::Debug for Number { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Small(..) => write!(f, "Number::Small( {} )", self.to_string(false)), - Self::Big(..) => write!(f, "Number::Big( {} )", self.to_string(false)), - } - } -} - -impl ToPrimitive for Number { - fn to_u64(&self) -> Option { - match self { - Self::Small(n) => { - if !n.denom().is_one() { - return None; - } - n.to_u64() - } - Self::Big(n) => { - if !n.denom().is_one() { - return None; - } - n.to_u64() - } - } - } - - fn to_i64(&self) -> Option { - match self { - Self::Small(n) => { - if !n.denom().is_one() { - return None; - } - n.to_i64() - } - Self::Big(n) => { - if !n.denom().is_one() { - return None; - } - n.to_i64() - } - } - } -} - -impl Number { - pub(crate) fn inspect(&self) -> String { - self.to_string(false) - } - - pub(crate) fn to_string(&self, is_compressed: bool) -> String { - let mut whole = self.to_integer().abs(); - let has_decimal = self.is_decimal(); - let mut frac = self.abs().fract(); - let mut dec = String::with_capacity(if has_decimal { PRECISION } else { 0 }); - - let mut buf = String::new(); - - if has_decimal { - for _ in 0..(PRECISION - 1) { - frac *= 10_i64; - dec.push_str(&frac.to_integer().to_string()); - - frac = frac.fract(); - if frac.is_zero() { - break; - } - } - if !frac.is_zero() { - let end = (frac * 10_i64).round().to_integer(); - if end.is_ten() { - loop { - match dec.pop() { - Some('9') => continue, - Some(c) => { - dec.push(char::from(c as u8 + 1)); - break; - } - None => { - whole += 1; - break; - } - } - } - } else if end.is_zero() { - loop { - match dec.pop() { - Some('0') => continue, - Some(c) => { - dec.push(c); - break; - } - None => break, - } - } - } else { - dec.push_str(&end.to_string()); - } - } - } - - let has_decimal = !dec.is_empty(); - - if self.is_negative() && (!whole.is_zero() || has_decimal) { - buf.push('-'); - } - - // if the entire number is just zero, we always want to emit it - if whole.is_zero() && !has_decimal { - return "0".to_owned(); - - // otherwise, if the number is not 0, or the number before the decimal - // _is_ 0 and we aren't in compressed mode, emit the number before the - // decimal - } else if !(whole.is_zero() && is_compressed) { - buf.push_str(&whole.to_string()); - } - - if has_decimal { - buf.push('.'); - buf.push_str(&dec); - } - - buf - } -} - -impl PartialOrd for Number { - fn partial_cmp(&self, other: &Self) -> Option { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => val1.partial_cmp(val2), - Self::Big(val2) => { - let tuple: (i64, i64) = (*val1).into(); - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) - .partial_cmp(val2) - } - }, - Self::Big(val1) => match other { - Self::Small(val2) => { - let tuple: (i64, i64) = (*val2).into(); - (**val1).partial_cmp(&BigRational::new_raw( - BigInt::from(tuple.0), - BigInt::from(tuple.1), - )) - } - Self::Big(val2) => val1.partial_cmp(val2), - }, - } - } -} - -impl Ord for Number { - fn cmp(&self, other: &Self) -> Ordering { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => val1.cmp(val2), - Self::Big(val2) => { - let tuple: (i64, i64) = (*val1).into(); - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)).cmp(val2) - } - }, - Self::Big(val1) => match other { - Self::Small(val2) => { - let tuple: (i64, i64) = (*val2).into(); - (**val1).cmp(&BigRational::new_raw( - BigInt::from(tuple.0), - BigInt::from(tuple.1), - )) - } - Self::Big(val2) => val1.cmp(val2), - }, - } - } -} - -impl Add for Number { - type Output = Self; - - fn add(self, other: Self) -> Self { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => match val1.checked_add(&val2) { - Some(v) => Self::Small(v), - None => { - let tuple1: (i64, i64) = val1.into(); - let tuple2: (i64, i64) = val2.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple1.0), BigInt::from(tuple1.1)) - + BigRational::new_raw( - BigInt::from(tuple2.0), - BigInt::from(tuple2.1), - ), - )) - } - }, - Self::Big(val2) => { - let tuple: (i64, i64) = val1.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) + *val2, - )) - } - }, - Self::Big(val1) => match other { - Self::Big(val2) => Self::Big(Box::new(*val1 + *val2)), - Self::Small(val2) => { - let tuple: (i64, i64) = val2.into(); - Self::Big(Box::new( - (*val1) - + BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - )) - } - }, - } - } -} - -impl Add<&Self> for Number { - type Output = Self; - - fn add(self, other: &Self) -> Self { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => match val1.checked_add(val2) { - Some(v) => Self::Small(v), - None => { - let tuple1: (i64, i64) = val1.into(); - let tuple2: (i64, i64) = (*val2).into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple1.0), BigInt::from(tuple1.1)) - + BigRational::new_raw( - BigInt::from(tuple2.0), - BigInt::from(tuple2.1), - ), - )) - } - }, - Self::Big(val2) => { - let tuple: (i64, i64) = val1.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) - + *val2.clone(), - )) - } - }, - Self::Big(val1) => match other { - Self::Big(val2) => Self::Big(Box::new(*val1 + *val2.clone())), - Self::Small(val2) => { - let tuple: (i64, i64) = (*val2).into(); - Self::Big(Box::new( - *val1 + BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - )) - } - }, - } - } -} - -impl AddAssign for Number { - fn add_assign(&mut self, other: Self) { - let tmp = mem::take(self); - *self = tmp + other; - } -} - -impl Sub for Number { - type Output = Self; - - fn sub(self, other: Self) -> Self { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => match val1.checked_sub(&val2) { - Some(v) => Self::Small(v), - None => { - let tuple1: (i64, i64) = val1.into(); - let tuple2: (i64, i64) = val2.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple1.0), BigInt::from(tuple1.1)) - - BigRational::new_raw( - BigInt::from(tuple2.0), - BigInt::from(tuple2.1), - ), - )) - } - }, - Self::Big(val2) => { - let tuple: (i64, i64) = val1.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) - *val2, - )) - } - }, - Self::Big(val1) => match other { - Self::Big(val2) => Self::Big(Box::new(*val1 - *val2)), - Self::Small(val2) => { - let tuple: (i64, i64) = val2.into(); - Self::Big(Box::new( - *val1 - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - )) - } - }, - } - } -} - -impl SubAssign for Number { - fn sub_assign(&mut self, other: Self) { - let tmp = mem::take(self); - *self = tmp - other; - } -} - -impl Mul for Number { - type Output = Self; - - fn mul(self, other: Self) -> Self { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => match val1.checked_mul(&val2) { - Some(v) => Self::Small(v), - None => { - let tuple1: (i64, i64) = val1.into(); - let tuple2: (i64, i64) = val2.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple1.0), BigInt::from(tuple1.1)) - * BigRational::new_raw( - BigInt::from(tuple2.0), - BigInt::from(tuple2.1), - ), - )) - } - }, - Self::Big(val2) => { - let tuple: (i64, i64) = val1.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) * *val2, - )) - } - }, - Self::Big(val1) => match other { - Self::Big(val2) => Self::Big(Box::new(*val1 * *val2)), - Self::Small(val2) => { - let tuple: (i64, i64) = val2.into(); - Self::Big(Box::new( - *val1 * BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - )) - } - }, - } - } -} - -impl Mul for Number { - type Output = Self; - - fn mul(self, other: i64) -> Self { - match self { - Self::Small(val1) => Self::Small(val1 * other), - Self::Big(val1) => Self::Big(Box::new(*val1 * BigInt::from(other))), - } - } -} - -impl MulAssign for Number { - fn mul_assign(&mut self, other: i64) { - let tmp = mem::take(self); - *self = tmp * other; - } -} - -impl MulAssign for Number { - fn mul_assign(&mut self, other: Self) { - let tmp = mem::take(self); - *self = tmp * other; - } -} - -impl Div for Number { - type Output = Self; - - fn div(self, other: Self) -> Self { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => match val1.checked_div(&val2) { - Some(v) => Self::Small(v), - None => { - let tuple1: (i64, i64) = val1.into(); - let tuple2: (i64, i64) = val2.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple1.0), BigInt::from(tuple1.1)) - / BigRational::new_raw( - BigInt::from(tuple2.0), - BigInt::from(tuple2.1), - ), - )) - } - }, - Self::Big(val2) => { - let tuple: (i64, i64) = val1.into(); - Self::Big(Box::new( - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)) / *val2, - )) - } - }, - Self::Big(val1) => match other { - Self::Big(val2) => Self::Big(Box::new(*val1 / *val2)), - Self::Small(val2) => { - let tuple: (i64, i64) = val2.into(); - Self::Big(Box::new( - *val1 / BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - )) - } - }, - } - } -} - -impl DivAssign for Number { - fn div_assign(&mut self, other: Self) { - let tmp = mem::take(self); - *self = tmp / other; - } -} - -fn modulo(n1: BigRational, n2: BigRational) -> BigRational { - (n1 % n2.clone() + n2.clone()) % n2 -} - -impl Rem for Number { - type Output = Self; - - fn rem(self, other: Self) -> Self { - match self { - Self::Small(val1) => match other { - Self::Small(val2) => { - let tuple1: (i64, i64) = val1.into(); - let tuple2: (i64, i64) = val2.into(); - - Self::Big(Box::new(modulo( - BigRational::new_raw(BigInt::from(tuple1.0), BigInt::from(tuple1.1)), - BigRational::new_raw(BigInt::from(tuple2.0), BigInt::from(tuple2.1)), - ))) - } - Self::Big(val2) => { - let tuple: (i64, i64) = val1.into(); - - Self::Big(Box::new(modulo( - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - *val2, - ))) - } - }, - Self::Big(val1) => match other { - Self::Big(val2) => Self::Big(Box::new(modulo(*val1, *val2))), - Self::Small(val2) => { - let tuple: (i64, i64) = val2.into(); - Self::Big(Box::new(modulo( - *val1, - BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)), - ))) - } - }, - } - } -} - -impl RemAssign for Number { - fn rem_assign(&mut self, other: Self) { - let tmp = mem::take(self); - *self = tmp % other; - } -} - -impl Neg for Number { - type Output = Self; - - fn neg(self) -> Self { - match self { - Self::Small(v) => Self::Small(-v), - Self::Big(v) => Self::Big(Box::new(-*v)), - } - } -} diff --git a/src/value/sass_function.rs b/src/value/sass_function.rs index a193ca3f..8da6673d 100644 --- a/src/value/sass_function.rs +++ b/src/value/sass_function.rs @@ -11,11 +11,15 @@ use std::fmt; -use codemap::Spanned; +// use codemap::Spanned; use crate::{ - args::CallArgs, atrule::Function, builtin::Builtin, common::Identifier, error::SassResult, - parse::Parser, value::Value, + // error::SassResult, + ast::AstFunctionDecl, + // value::Value, + builtin::Builtin, + common::Identifier, + evaluate::Environment, }; /// A Sass function @@ -26,20 +30,37 @@ use crate::{ /// for use in the builtin function `inspect()` #[derive(Clone, Eq, PartialEq)] pub(crate) enum SassFunction { + // todo: Cow<'static>? Builtin(Builtin, Identifier), - UserDefined { - function: Box, - name: Identifier, - }, + // todo: maybe arc? + UserDefined(UserDefinedFunction), + Plain { name: Identifier }, } +#[derive(Debug, Clone)] +pub(crate) struct UserDefinedFunction { + pub function: Box, + pub name: Identifier, + pub env: Environment, +} + +impl PartialEq for UserDefinedFunction { + fn eq(&self, other: &Self) -> bool { + self.function == other.function && self.name == other.name + } +} + +impl Eq for UserDefinedFunction {} + impl SassFunction { /// Get the name of the function referenced /// /// Used mainly in debugging and `inspect()` - pub fn name(&self) -> &Identifier { + pub fn name(&self) -> Identifier { match self { - Self::Builtin(_, name) | Self::UserDefined { name, .. } => name, + Self::Builtin(_, name) + | Self::UserDefined(UserDefinedFunction { name, .. }) + | Self::Plain { name } => *name, } } @@ -48,22 +69,11 @@ impl SassFunction { /// Used only in `std::fmt::Debug` for `SassFunction` fn kind(&self) -> &'static str { match &self { + Self::Plain { .. } => "Plain", Self::Builtin(..) => "Builtin", Self::UserDefined { .. } => "UserDefined", } } - - pub fn call( - self, - args: CallArgs, - module: Option>, - parser: &mut Parser, - ) -> SassResult { - match self { - Self::Builtin(f, ..) => f.0(args, parser), - Self::UserDefined { function, .. } => parser.eval_function(*function, args, module), - } - } } impl fmt::Debug for SassFunction { diff --git a/src/value/sass_number.rs b/src/value/sass_number.rs new file mode 100644 index 00000000..9326961e --- /dev/null +++ b/src/value/sass_number.rs @@ -0,0 +1,291 @@ +use std::ops::{Add, Div, Mul, Sub}; + +use codemap::Span; + +use crate::{ + error::SassResult, + serializer::inspect_number, + unit::{are_any_convertible, known_compatibilities_by_unit, Unit, UNIT_CONVERSION_TABLE}, + Options, +}; + +use super::Number; + +// todo: is as_slash included in eq +#[derive(Debug, Clone)] +pub(crate) struct SassNumber { + pub num: Number, + pub unit: Unit, + pub as_slash: Option>, +} + +pub(crate) fn conversion_factor(from: &Unit, to: &Unit) -> Option { + if from == to { + return Some(1.0); + } + + UNIT_CONVERSION_TABLE.get(to)?.get(from).copied() +} + +impl SassNumber { + pub fn has_comparable_units(&self, other_unit: &Unit) -> bool { + self.unit.comparable(other_unit) + } + + /// Unlike [`SassNumber::has_comparable_units`], this considers `Unit::None` + /// to be compatible only with itself + pub fn has_compatible_units(&self, other_unit: &Unit) -> bool { + if (self.unit == Unit::None || *other_unit == Unit::None) && self.unit != *other_unit { + return false; + } + + self.has_comparable_units(other_unit) + } + + #[allow(clippy::collapsible_if)] + pub fn multiply_units(&self, mut num: f64, other_unit: Unit) -> SassNumber { + let (numer_units, denom_units) = self.unit.clone().numer_and_denom(); + let (other_numer, other_denom) = other_unit.numer_and_denom(); + + if numer_units.is_empty() { + if other_denom.is_empty() && !are_any_convertible(&denom_units, &other_numer) { + return SassNumber { + num: Number(num), + unit: Unit::new(other_numer, denom_units), + as_slash: None, + }; + } else if denom_units.is_empty() { + return SassNumber { + num: Number(num), + unit: Unit::new(other_numer, other_denom), + as_slash: None, + }; + } + } else if other_numer.is_empty() { + if other_denom.is_empty() + || (denom_units.is_empty() && !are_any_convertible(&numer_units, &other_denom)) + { + return SassNumber { + num: Number(num), + unit: Unit::new(numer_units, other_denom), + as_slash: None, + }; + } + } + + let mut new_numer = Vec::new(); + + let mut mutable_other_denom = other_denom; + + for numer in numer_units { + let mut has_removed = false; + mutable_other_denom.retain(|denom| { + if has_removed { + return true; + } + + if let Some(factor) = conversion_factor(denom, &numer) { + num /= factor; + has_removed = true; + return false; + } + + true + }); + + if !has_removed { + new_numer.push(numer); + } + } + + let mut mutable_denom = denom_units; + for numer in other_numer { + let mut has_removed = false; + mutable_denom.retain(|denom| { + if has_removed { + return true; + } + + if let Some(factor) = conversion_factor(denom, &numer) { + num /= factor; + has_removed = true; + return false; + } + + true + }); + + if !has_removed { + new_numer.push(numer); + } + } + + mutable_denom.append(&mut mutable_other_denom); + + SassNumber { + num: Number(num), + unit: Unit::new(new_numer, mutable_denom), + as_slash: None, + } + } + + pub fn assert_no_units(&self, name: &str, span: Span) -> SassResult<()> { + if self.unit == Unit::None { + Ok(()) + } else { + Err(( + format!( + "${name}: Expected {} to have no units.", + inspect_number(self, &Options::default(), span)? + ), + span, + ) + .into()) + } + } + + pub fn assert_unit(&self, unit: &Unit, name: &str, span: Span) -> SassResult<()> { + if self.unit == *unit { + Ok(()) + } else { + Err(( + format!( + "${name}: Expected {} to have unit \"{unit}\".", + inspect_number(self, &Options::default(), span)? + ), + span, + ) + .into()) + } + } + + pub fn is_comparable_to(&self, other: &Self) -> bool { + self.unit.comparable(&other.unit) + } + + /// For use in calculations + pub fn has_possibly_compatible_units(&self, other: &Self) -> bool { + if self.unit.is_complex() || other.unit.is_complex() { + return false; + } + + let known_compatibilities = match known_compatibilities_by_unit(&self.unit) { + Some(known_compatibilities) => known_compatibilities, + None => return true, + }; + + known_compatibilities.contains(&other.unit) + || known_compatibilities_by_unit(&other.unit).is_none() + } + + // todo: remove + pub fn num(&self) -> Number { + self.num + } + + // todo: remove + pub fn unit(&self) -> &Unit { + &self.unit + } +} + +impl PartialEq for SassNumber { + fn eq(&self, other: &Self) -> bool { + self.num == other.num && self.unit == other.unit + } +} + +impl Add for SassNumber { + type Output = SassNumber; + fn add(self, rhs: SassNumber) -> Self::Output { + if self.unit == rhs.unit { + SassNumber { + num: self.num + rhs.num, + unit: self.unit, + as_slash: None, + } + } else if self.unit == Unit::None { + SassNumber { + num: self.num + rhs.num, + unit: rhs.unit, + as_slash: None, + } + } else if rhs.unit == Unit::None { + SassNumber { + num: self.num + rhs.num, + unit: self.unit, + as_slash: None, + } + } else { + SassNumber { + num: self.num + rhs.num.convert(&rhs.unit, &self.unit), + unit: self.unit, + as_slash: None, + } + } + } +} + +impl Sub for SassNumber { + type Output = SassNumber; + + fn sub(self, rhs: SassNumber) -> Self::Output { + if self.unit == rhs.unit { + SassNumber { + num: self.num - rhs.num, + unit: self.unit, + as_slash: None, + } + } else if self.unit == Unit::None { + SassNumber { + num: self.num - rhs.num, + unit: rhs.unit, + as_slash: None, + } + } else if rhs.unit == Unit::None { + SassNumber { + num: self.num - rhs.num, + unit: self.unit, + as_slash: None, + } + } else { + SassNumber { + num: self.num - rhs.num.convert(&rhs.unit, &self.unit), + unit: self.unit, + as_slash: None, + } + } + } +} + +impl Mul for SassNumber { + type Output = SassNumber; + fn mul(self, rhs: SassNumber) -> Self::Output { + if rhs.unit == Unit::None { + return SassNumber { + num: self.num * rhs.num, + unit: self.unit, + as_slash: None, + }; + } + + self.multiply_units(self.num.0 * rhs.num.0, rhs.unit) + } +} + +impl Div for SassNumber { + type Output = SassNumber; + fn div(self, rhs: SassNumber) -> Self::Output { + if rhs.unit == Unit::None { + return SassNumber { + num: self.num / rhs.num, + unit: self.unit, + as_slash: None, + }; + } + + self.multiply_units(self.num.0 / rhs.num.0, rhs.unit.invert()) + } +} + +impl Eq for SassNumber {} diff --git a/tests/addition.rs b/tests/addition.rs index 153ca347..4dfce744 100644 --- a/tests/addition.rs +++ b/tests/addition.rs @@ -380,3 +380,57 @@ error!( "a {color: 1 + get-function(lighten);}", "Error: get-function(\"lighten\") isn't a valid CSS value." ); +error!( + add_two_calculations, + "a {color: calc(1rem + 1px) + calc(1rem + 1px);}", + r#"Error: Undefined operation "calc(1rem + 1px) + calc(1rem + 1px)"."# +); +error!( + num_plus_calculation, + "a {color: 1 + calc(1rem + 1px);}", r#"Error: Undefined operation "1 + calc(1rem + 1px)"."# +); +test!( + quoted_string_plus_calculation, + "a {\n color: \"a\" + calc(1px + 1%);\n}\n", + "a {\n color: \"acalc(1px + 1%)\";\n}\n" +); +test!( + calculation_plus_quoted_string, + "a {\n color: calc(1px + 1%) + \"a\";\n}\n", + "a {\n color: \"calc(1px + 1%)a\";\n}\n" +); +test!( + empty_quoted_string_plus_calculation, + "a {\n color: \"\" + calc(1px + 1%);\n}\n", + "a {\n color: \"calc(1px + 1%)\";\n}\n" +); +test!( + calculation_plus_empty_quoted_string, + "a {\n color: calc(1px + 1%) + \"\";\n}\n", + "a {\n color: \"calc(1px + 1%)\";\n}\n" +); +test!( + unquoted_string_plus_calculation, + "a {\n color: foo + calc(1px + 1%);\n}\n", + "a {\n color: foocalc(1px + 1%);\n}\n" +); +test!( + calculation_plus_unquoted_string, + "a {\n color: calc(1px + 1%) + foo;\n}\n", + "a {\n color: calc(1px + 1%)foo;\n}\n" +); +test!( + num_plus_nan, + "a {\n color: 1 + (0/0);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + nan_plus_num, + "a {\n color: (0/0) + 1;\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + nan_plus_nan, + "a {\n color: (0/0) + (0/0);\n}\n", + "a {\n color: NaN;\n}\n" +); diff --git a/tests/and.rs b/tests/and.rs index 0d9f03de..89af1bc6 100644 --- a/tests/and.rs +++ b/tests/and.rs @@ -54,7 +54,6 @@ test!( "a {\n color: false;\n}\n" ); error!( - #[ignore = "blocked on a rewrite of value eval"] properly_bubbles_error_when_invalid_char_after_and, - "a {\n color: false and? foo;\n}\n", "Error: expected \";\"." + "a {\n color: false and? foo;\n}\n", "Error: Expected expression." ); diff --git a/tests/args.rs b/tests/args.rs index e8c11241..a98ce30a 100644 --- a/tests/args.rs +++ b/tests/args.rs @@ -67,7 +67,6 @@ test!( "a {\n color: red;\n}\n" ); error!( - #[ignore = "expects incorrect char, '{'"] nothing_after_open, "a { color:rgb(; }", "Error: expected \")\"." ); @@ -170,7 +169,7 @@ error!( ); error!( filter_nothing_before_equal, - "a {\n color: foo(=a);\n}\n", "Error: Expected expression." + "a {\n color: foo(=a);\n}\n", "Error: expected \")\"." ); error!( filter_nothing_after_equal, @@ -275,3 +274,80 @@ error!( }", "Error: expected \")\"." ); +error!( + duplicate_named_arg, + "@function foo($a) { + @return $a; + } + + a { + color: foo($a: red, $a: green); + }", + "Error: Duplicate argument." +); +error!( + keyword_arg_before_positional, + "@function foo($a, $b) { + @return $a, $b; + } + + a { + color: foo($a: red, green); + }", + "Error: Positional arguments must come before keyword arguments." +); +error!( + duplicate_arg_in_declaration, + "@function foo($a, $a) { + @return $a; + }", + "Error: Duplicate argument." +); +error!( + variable_keyword_args_is_list, + "@function foo($a...) { + @return inspect($a); + } + + a { + color: foo(a..., a b...); + }", + "Error: Variable keyword arguments must be a map (was a b)." +); +error!( + keyword_arg_to_function_expecting_varargs, + "a {\n color: zip(a, b, $a: c);\n}\n", "Error: No argument named $a." +); +error!( + too_many_keyword_args_passed_one_extra_arg, + "@function foo($a) { + @return $a; + } + + a { + color: foo($a: red, $b: green); + }", + "Error: No argument named $b." +); +error!( + too_many_keyword_args_passed_two_extra_args, + "@function foo($a) { + @return $a; + } + + a { + color: foo($a: red, $b: green, $c: blue); + }", + "Error: No arguments named $b or $c." +); +error!( + too_many_keyword_args_passed_three_extra_args, + "@function foo($a) { + @return $a; + } + + a { + color: foo($a: red, $b: green, $c: blue, $d: brown); + }", + "Error: No arguments named $b, $c or $d." +); diff --git a/tests/at-root.rs b/tests/at-root.rs index d18d2120..7fce4afe 100644 --- a/tests/at-root.rs +++ b/tests/at-root.rs @@ -81,6 +81,25 @@ test!( }", "b {\n color: red;\n}\n\na b {\n color: red;\n}\n" ); +test!( + at_root_between_other_styles_is_emitted_with_same_order, + "a { + a { + color: red; + } + + @at-root { + b { + color: red; + } + } + + c { + color: red; + } + }", + "a a {\n color: red;\n}\nb {\n color: red;\n}\n\na c {\n color: red;\n}\n" +); test!( no_newline_between_style_rules_when_there_exists_a_selector, "@at-root a { @@ -127,12 +146,136 @@ test!( }", "@media screen {\n a {\n color: red;\n }\n}\n" ); +test!( + simple_at_root_query, + "a { + @at-root (with: rule) { + b: c; + } + }", + "a {\n b: c;\n}\n" +); +test!( + without_media_inside_media_rule, + "@media (min-width: 1337px) { + .foo { + content: baz; + } + + @at-root (without: media) { + .foo { + content: bar; + } + } + }", + "@media (min-width: 1337px) {\n .foo {\n content: baz;\n }\n}\n.foo {\n content: bar;\n}\n" +); +test!( + with_media_inside_media_rule_inside_supports_rule, + "@media (min-width: 1337px) { + @supports (color: red) { + @at-root (with: media) { + .foo { + content: bar; + } + } + } + }", + "@media (min-width: 1337px) {\n .foo {\n content: bar;\n }\n}\n" +); +test!( + with_media_and_supports_inside_media_rule_inside_supports_rule, + "@media (min-width: 1337px) { + @supports (color: red) { + @at-root (with: media supports) { + .foo { + content: bar; + } + } + } + }", + "@media (min-width: 1337px) {\n @supports (color: red) {\n .foo {\n content: bar;\n }\n }\n}\n" +); +test!( + without_keyframes_inside_keyframes, + "@keyframes animation { + @at-root (without: keyframes) { + to { + color: red; + } + } + }", + "@keyframes animation {}\nto {\n color: red;\n}\n" +); +test!( + at_root_has_its_own_scope, + "$root_default: initial; + $root_implicit: initial; + $root_explicit: initial !global; + + @at-root { + $root_implicit: outer; + $root_explicit: outer !global; + $root_default: outer !default; + $local_implicit: outer; + $local_explicit: outer !global; + $local_default: outer !default; + + @at-root { + $root_implicit: inner; + $root_explicit: inner !global; + $root_default: inner !default; + $local_implicit: inner; + $local_explicit: inner !global; + $local_default: inner !default; + } + } + + result { + root_default: $root_default; + root_implicit: $root_implicit; + root_explicit: $root_explicit; + + @if variable-exists(local_default) { + local_default: $local_default; + } + + @if variable-exists(local_implicit) { + local_implicit: $local_implicit; + } + + @if variable-exists(local_explicit) { + local_explicit: $local_explicit; + } + }", + "result {\n root_default: initial;\n root_implicit: initial;\n root_explicit: inner;\n local_explicit: inner;\n}\n" +); +test!( + #[ignore = "we currently emit the empty unknown-at-rule"] + inside_style_inside_unknown_at_rule, + "@unknown { + .foo { + @at-root .bar { + a: b + } + } + }", + "@unknown {\n .bar {\n a: b;\n }\n}\n" +); error!( - #[ignore = "we do not currently validate missing closing curly braces"] missing_closing_curly_brace, "@at-root {", "Error: expected \"}\"." ); error!( style_at_toplevel_without_selector, - "@at-root { color: red; }", "Error: Found style at the toplevel inside @at-root." + "@at-root { color: red; }", "Error: expected \"{\"." +); +error!( + extend_inside_at_root_would_be_put_at_root_of_document, + "a { + @at-root { + @extend b; + } + }", + "Error: @extend may only be used within style rules." ); diff --git a/tests/calc_args.rs b/tests/calc_args.rs new file mode 100644 index 00000000..8ef394d0 --- /dev/null +++ b/tests/calc_args.rs @@ -0,0 +1,12 @@ +#[macro_use] +mod macros; + +test!( + arg_is_binop, + "@use \"sass:meta\"; + + a { + color: meta.calc-args(calc(1vh + 1px)); + }", + "a {\n color: 1vh + 1px;\n}\n" +); diff --git a/tests/charset.rs b/tests/charset.rs index bcb7d026..568ca286 100644 --- a/tests/charset.rs +++ b/tests/charset.rs @@ -16,6 +16,31 @@ test!( "@charset \"foo\";\na {\n color: red;\n}\n", "a {\n color: red;\n}\n" ); +test!( + comment_between_rule_and_string, + "@charset/**/\"foo\";\na {\n color: red;\n}\n", + "a {\n color: red;\n}\n" +); +test!( + comment_after_string, + "@charset \"foo\"/**/;\na {\n color: red;\n}\n", + "/**/\na {\n color: red;\n}\n" +); +test!( + no_space_after_at_rule, + "@charset\"foo\";\na {\n color: red;\n}\n", + "a {\n color: red;\n}\n" +); +test!( + charset_inside_rule, + "a {\n color: red;@charset \"foo\";\n\n}\n", + "a {\n color: red;\n @charset \"foo\";\n}\n" +); +test!( + charset_after_rule, + "a {\n color: red;\n}\n@charset \"foo\";\n", + "a {\n color: red;\n}\n" +); error!( invalid_charset_value, "@charset 1;", "Error: Expected string." @@ -24,3 +49,11 @@ error!( invalid_charset_value_unquoted_string, "@charset a;", "Error: Expected string." ); +error!( + invalid_charset_value_silent_comment, + "@charset //", "Error: Expected string." +); +error!( + invalid_charset_value_unterminated_loud_comment, + "@charset /*", "Error: expected more input." +); diff --git a/tests/clamp.rs b/tests/clamp.rs new file mode 100644 index 00000000..db01f04f --- /dev/null +++ b/tests/clamp.rs @@ -0,0 +1,40 @@ +#[macro_use] +mod macros; + +error!( + clamp_empty_args, + "a {\n color: clamp();\n}\n", "Error: Expected number, variable, function, or calculation." +); +error!( + clamp_parens_in_args, + "a {\n color: clamp((()));\n}\n", + "Error: Expected number, variable, function, or calculation." +); +error!( + clamp_single_arg, + "a {\n color: clamp(1);\n}\n", "Error: 3 arguments required, but only 1 was passed." +); +test!( + clamp_all_unitless, + "a {\n color: clamp(1, 2, 3);\n}\n", + "a {\n color: 2;\n}\n" +); +test!( + clamp_all_same_unit, + "a {\n color: clamp(1px, 2px, 3px);\n}\n", + "a {\n color: 2px;\n}\n" +); +test!( + clamp_last_non_comparable_but_compatible, + "a {\n color: clamp(1px, 2px, 3vh);\n}\n", + "a {\n color: clamp(1px, 2px, 3vh);\n}\n" +); +test!( + clamp_last_comparable_does_unit_conversion, + "a {\n color: clamp(1px, 1in, 1cm);\n}\n", + "a {\n color: 1cm;\n}\n" +); +error!( + clamp_last_non_compatible, + "a {\n color: clamp(1px, 2px, 3deg);\n}\n", "Error: 1px and 3deg are incompatible." +); diff --git a/tests/color.rs b/tests/color.rs index c2161972..f6d95f9b 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -79,27 +79,27 @@ test!( test!( converts_rgb_to_named_color, "a {\n color: rgb(0, 0, 0);\n}\n", - "a {\n color: black;\n}\n" + "a {\n color: rgb(0, 0, 0);\n}\n" ); test!( converts_rgba_to_named_color_red, "a {\n color: rgb(255, 0, 0, 255);\n}\n", - "a {\n color: red;\n}\n" + "a {\n color: rgb(255, 0, 0);\n}\n" ); test!( rgb_negative, "a {\n color: rgb(-1, 1, 1);\n}\n", - "a {\n color: #000101;\n}\n" + "a {\n color: rgb(0, 1, 1);\n}\n" ); test!( rgb_binop, "a {\n color: rgb(1, 2, 1+2);\n}\n", - "a {\n color: #010203;\n}\n" + "a {\n color: rgb(1, 2, 3);\n}\n" ); test!( rgb_pads_0, "a {\n color: rgb(1, 2, 3);\n}\n", - "a {\n color: #010203;\n}\n" + "a {\n color: rgb(1, 2, 3);\n}\n" ); test!( rgba_percent, @@ -114,12 +114,12 @@ test!( test!( rgb_double_digits, "a {\n color: rgb(254, 255, 255);\n}\n", - "a {\n color: #feffff;\n}\n" + "a {\n color: rgb(254, 255, 255);\n}\n" ); test!( rgb_double_digits_white, "a {\n color: rgb(255, 255, 255);\n}\n", - "a {\n color: white;\n}\n" + "a {\n color: rgb(255, 255, 255);\n}\n" ); test!( alpha_function_4_hex, @@ -144,7 +144,7 @@ test!( test!( rgba_one_arg, "a {\n color: rgba(1 2 3);\n}\n", - "a {\n color: #010203;\n}\n" + "a {\n color: rgb(1, 2, 3);\n}\n" ); test!( rgb_two_args, @@ -159,7 +159,7 @@ test!( test!( rgba_opacity_over_1, "a {\n color: rgba(1, 2, 3, 3);\n}\n", - "a {\n color: #010203;\n}\n" + "a {\n color: rgb(1, 2, 3);\n}\n" ); test!( rgba_negative_alpha, @@ -179,7 +179,7 @@ test!( test!( rgba_3_args, "a {\n color: rgba(7.1%, 20.4%, 33.9%);\n}\n", - "a {\n color: #123456;\n}\n" + "a {\n color: rgb(18, 52, 86);\n}\n" ); error!( rgb_no_args, @@ -383,30 +383,30 @@ test!( test!( rgba_1_arg, "a {\n color: rgba(74.7% 173 93%);\n}\n", - "a {\n color: #beaded;\n}\n" + "a {\n color: rgb(190, 173, 237);\n}\n" ); test!( hsla_1_arg, "a {\n color: hsla(60 60% 50%);\n}\n", - "a {\n color: #cccc33;\n}\n" + "a {\n color: hsl(60deg, 60%, 50%);\n}\n" ); test!( hsla_1_arg_weird_units, "a {\n color: hsla(60foo 60foo 50foo);\n}\n", - "a {\n color: #cccc33;\n}\n" + "a {\n color: hsl(60deg, 60%, 50%);\n}\n" ); test!( sass_spec__spec_colors_basic, - "p { + r#"p { color: rgb(255, 128, 0); color: red green blue; color: (red) (green) (blue); color: red + hux; - color: unquote(\"red\") + green; + color: unquote("red") + green; foo: rgb(200, 150%, 170%); } -", - "p {\n color: #ff8000;\n color: red green blue;\n color: red green blue;\n color: redhux;\n color: redgreen;\n foo: #c8ffff;\n}\n" +"#, + "p {\n color: rgb(255, 128, 0);\n color: red green blue;\n color: red green blue;\n color: redhux;\n color: redgreen;\n foo: rgb(200, 255, 255);\n}\n" ); test!( sass_spec__spec_colors_change_color, @@ -432,7 +432,7 @@ test!( test!( negative_values_in_rgb, "a {\n color: rgb(-1 -1 -1);\n}\n", - "a {\n color: black;\n}\n" + "a {\n color: rgb(0, 0, 0);\n}\n" ); test!( interpolation_after_hash_containing_only_hex_chars, @@ -462,7 +462,7 @@ test!( test!( all_three_rgb_channels_have_decimal, "a {\n color: rgba(1.5, 1.5, 1.5, 1);\n}\n", - "a {\n color: #020202;\n}\n" + "a {\n color: rgb(2, 2, 2);\n}\n" ); test!( builtin_fn_red_rounds_channel, @@ -527,19 +527,19 @@ error!( ); // todo: we need many more of these tests test!( - rgba_special_fn_4th_arg_max, + rgba_one_arg_special_fn_4th_arg_max, "a {\n color: rgba(1 2 max(3, 3));\n}\n", - "a {\n color: rgba(1, 2, max(3, 3));\n}\n" + "a {\n color: rgb(1, 2, 3);\n}\n" ); test!( rgb_special_fn_4_arg_maintains_units, "a {\n color: rgb(1, 0.02, 3%, max(0.4));\n}\n", - "a {\n color: rgb(1, 0.02, 3%, max(0.4));\n}\n" + "a {\n color: rgba(1, 0, 8, 0.4);\n}\n" ); test!( rgb_special_fn_3_arg_maintains_units, "a {\n color: rgb(1, 0.02, max(0.4));\n}\n", - "a {\n color: rgb(1, 0.02, max(0.4));\n}\n" + "a {\n color: rgb(1, 0, 0);\n}\n" ); test!( rgb_special_fn_2_arg_first_non_color, @@ -552,7 +552,6 @@ test!( "a {\n color: rgb(3, 1, 1, var(--foo));\n}\n" ); test!( - #[ignore = "we do not check if interpolation occurred"] interpolated_named_color_is_not_color, "a {\n color: type-of(r#{e}d);\n}\n", "a {\n color: string;\n}\n" @@ -587,3 +586,52 @@ test!( "a {\n color: hue(rgb(1, 2, 5));\n}\n", "a {\n color: 225deg;\n}\n" ); +test!( + rgb_3_args_first_arg_is_special_fn, + "a {\n color: rgb(env(--foo), 2, 3);\n}\n", + "a {\n color: rgb(env(--foo), 2, 3);\n}\n" +); +test!( + hsl_conversion_is_correct, + "a { + color: hue(red); + color: saturation(red); + color: lightness(red); + color: change-color(red, $lightness: 95%); + color: red(change-color(red, $lightness: 95%)); + color: blue(change-color(red, $lightness: 95%)); + color: green(change-color(red, $lightness: 95%)); + }", + "a {\n color: 0deg;\n color: 100%;\n color: 50%;\n color: #ffe6e6;\n color: 255;\n color: 230;\n color: 230;\n}\n" +); +test!( + rgb_two_arg_nan_alpha, + "a { + color: rgb(red, 0/0); + color: opacity(rgb(red, 0/0)); + }", + "a {\n color: red;\n color: 1;\n}\n" +); +error!( + rgb_more_than_4_args, + "a {\n color: rgb(59%, 169, 69%, 50%, 50%);\n}\n", + "Error: Only 4 arguments allowed, but 5 were passed." +); +error!( + rgba_more_than_4_args, + "a {\n color: rgba(59%, 169, 69%, 50%, 50%);\n}\n", + "Error: Only 4 arguments allowed, but 5 were passed." +); +error!( + opacify_amount_nan, + "a {\n color: opacify(#fff, (0/0));\n}\n", + "Error: $amount: Expected NaN to be within 0 and 1." +); +error!( + interpolated_string_is_not_color, + "a {\n color: red(r#{e}d);\n}\n", "Error: $color: red is not a color." +); +error!( + single_arg_saturate_expects_number, + "a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a number." +); diff --git a/tests/color_hsl.rs b/tests/color_hsl.rs index ec1ab8cf..f0b62b7c 100644 --- a/tests/color_hsl.rs +++ b/tests/color_hsl.rs @@ -12,47 +12,47 @@ error!( test!( hsl_basic, "a {\n color: hsl(193, 67%, 99);\n}\n", - "a {\n color: #fbfdfe;\n}\n" + "a {\n color: hsl(193deg, 67%, 99%);\n}\n" ); test!( hsla_basic, "a {\n color: hsla(193, 67%, 99, .6);\n}\n", - "a {\n color: rgba(251, 253, 254, 0.6);\n}\n" + "a {\n color: hsla(193deg, 67%, 99%, 0.6);\n}\n" ); test!( hsl_doesnt_care_about_units, "a {\n color: hsl(193deg, 67foo, 99%);\n}\n", - "a {\n color: #fbfdfe;\n}\n" + "a {\n color: hsl(193deg, 67%, 99%);\n}\n" ); test!( hsl_named, "a {\n color: hsl($hue: 193, $saturation: 67%, $lightness: 99);\n}\n", - "a {\n color: #fbfdfe;\n}\n" + "a {\n color: hsl(193deg, 67%, 99%);\n}\n" ); test!( hsl_four_args, "a {\n color: hsl(0, 0, 0, 0.456);\n}\n", - "a {\n color: rgba(0, 0, 0, 0.456);\n}\n" + "a {\n color: hsla(0deg, 0%, 0%, 0.456);\n}\n" ); test!( hsl_negative_hue, "a {\n color: hsl(-60deg, 100%, 50%);\n}\n", - "a {\n color: fuchsia;\n}\n" + "a {\n color: hsl(300deg, 100%, 50%);\n}\n" ); test!( hsl_hue_above_max, "a {\n color: hsl(540, 100%, 50%);\n}\n", - "a {\n color: aqua;\n}\n" + "a {\n color: hsl(180deg, 100%, 50%);\n}\n" ); test!( hsl_hue_below_min, "a {\n color: hsl(-540, 100%, 50%);\n}\n", - "a {\n color: aqua;\n}\n" + "a {\n color: hsl(180deg, 100%, 50%);\n}\n" ); test!( hsla_named, "a {\n color: hsla($hue: 193, $saturation: 67%, $lightness: 99, $alpha: .6);\n}\n", - "a {\n color: rgba(251, 253, 254, 0.6);\n}\n" + "a {\n color: hsla(193deg, 67%, 99%, 0.6);\n}\n" ); test!( hue, @@ -141,6 +141,40 @@ test!( // blocked on recognizing when to use 3-hex over 6-hex "a {\n color: #ee0000;\n}\n" ); +test!( + lighten_percent, + "a { + color: lighten(crimson, 10%); + }", + "a {\n color: #ed365b;\n}\n" +); +test!( + lighten_no_percent, + "a { + color: lighten(crimson, 10); + }", + "a {\n color: #ed365b;\n}\n" +); +test!( + channels_after_lighten, + "a { + color: red(lighten(crimson, 10)); + color: green(lighten(crimson, 10)); + color: blue(lighten(crimson, 10)); + color: hue(lighten(crimson, 10)); + color: hue(crimson); + color: saturation(lighten(crimson, 10)); + color: lightness(lighten(crimson, 10)); + }", + "a {\n color: 237;\n color: 54;\n color: 91;\n color: 348deg;\n color: 348deg;\n color: 83.3333333333%;\n color: 57.0588235294%;\n}\n" +); +error!( + lighten_nan, + "a { + color: lighten(crimson, (0/0)); + }", + "Error: $amount: Expected NaN to be within 0 and 100." +); test!( darken_named_args, "a {\n color: darken($color: hsl(25, 100%, 80%), $amount: 30%);\n}\n", @@ -163,6 +197,11 @@ test!( "a {\n color: saturate($color: hsl(25, 100%, 80%), $amount: 30%);\n}\n", "a {\n color: #ffc499;\n}\n" ); +test!( + saturation_cannot_go_above_100, + "a {\n color: saturation(saturate($color: hsl(25, 100%, 80%), $amount: 30%));\n}\n", + "a {\n color: 100%;\n}\n" +); test!( saturate_one_arg, "a {\n color: saturate($amount: 50%);\n}\n", @@ -201,22 +240,22 @@ test!( test!( negative_values_in_hsl, "a {\n color: hsl(-1 -1 -1);\n}\n", - "a {\n color: black;\n}\n" + "a {\n color: hsl(359deg, 0%, 0%);\n}\n" ); test!( hsla_becomes_named_color, "a {\n color: hsla(0deg, 100%, 50%);\n}\n", - "a {\n color: red;\n}\n" + "a {\n color: hsl(0deg, 100%, 50%);\n}\n" ); test!( hsl_special_fn_4_arg_maintains_units, "a {\n color: hsl(1, 0.02, 3%, max(0.4));\n}\n", - "a {\n color: hsl(1, 0.02, 3%, max(0.4));\n}\n" + "a {\n color: hsla(1deg, 0.02%, 3%, 0.4);\n}\n" ); test!( hsl_special_fn_3_arg_maintains_units, "a {\n color: hsl(1, 0.02, max(0.4));\n}\n", - "a {\n color: hsl(1, 0.02, max(0.4));\n}\n" + "a {\n color: hsl(1deg, 0.02%, 0.4%);\n}\n" ); test!( hsla_special_fn_1_arg_is_not_list, @@ -246,15 +285,25 @@ test!( test!( hsl_with_turn_unit, "a {\n color: hsl(8turn, 25%, 50%);\n}\n", - "a {\n color: #9f6860;\n}\n" + "a {\n color: hsl(8deg, 25%, 50%);\n}\n" ); test!( hsl_with_rad_unit, "a {\n color: hsl(8rad, 25%, 50%);\n}\n", - "a {\n color: #9f6860;\n}\n" + "a {\n color: hsl(8deg, 25%, 50%);\n}\n" ); test!( hsl_with_grad_unit, "a {\n color: hsl(8grad, 25%, 50%);\n}\n", - "a {\n color: #9f6860;\n}\n" + "a {\n color: hsl(8deg, 25%, 50%);\n}\n" +); +test!( + adjust_hue_nan, + "a {\n color: adjust-hue(hsla(200, 50%, 50%), (0/0));\n}\n", + "a {\n color: #404040;\n}\n" +); +test!( + adjust_hue_nan_get_hue, + "a {\n color: hue(adjust-hue(hsla(200, 50%, 50%), (0/0)));\n}\n", + "a {\n color: NaNdeg;\n}\n" ); diff --git a/tests/color_hwb.rs b/tests/color_hwb.rs index bd73b678..a6c161e7 100644 --- a/tests/color_hwb.rs +++ b/tests/color_hwb.rs @@ -76,6 +76,16 @@ test!( "@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 100%, -0.5);\n}\n", "a {\n color: rgba(0, 0, 0, 0);\n}\n" ); +test!( + hue_60_whiteness_20_blackness_100, + "@use \"sass:color\";\na {\n color: color.hwb(60, 20%, 100%);\n}\n", + "a {\n color: #2b2b2b;\n}\n" +); +test!( + one_arg_with_slash, + "@use \"sass:color\";\na {\n color: color.hwb(180 30% 40% / 0);\n}\n", + "a {\n color: rgba(77, 153, 153, 0);\n}\n" +); error!( hwb_whiteness_missing_pct, "@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n", diff --git a/tests/comments.rs b/tests/comments.rs index 1cd763db..d915198e 100644 --- a/tests/comments.rs +++ b/tests/comments.rs @@ -11,6 +11,11 @@ test!( "a {\n color: red /* hi */;\n}\n", "a {\n color: red;\n}\n" ); +test!( + removes_comment_before_style, + "a {\n color: /**/red;\n}\n", + "a {\n color: red;\n}\n" +); test!( preserves_outer_comments_before, "a {\n /* hi */\n color: red;\n}\n", @@ -26,6 +31,7 @@ test!( "a {\n /* foo */\n /* bar */\n color: red;\n}\n", "a {\n /* foo */\n /* bar */\n color: red;\n}\n" ); +test!(two_silent_comments_followed_by_eof, "//\n//\n", ""); test!( preserves_toplevel_comment_before, "/* foo */\na {\n color: red;\n}\n", @@ -36,6 +42,14 @@ test!( "a {\n color: red;\n}\n/* foo */\n", "a {\n color: red;\n}\n\n/* foo */\n" ); +test!( + #[ignore = "we use the old form of comment writing"] + preserves_trailing_comments, + "a { /**/ + color: red; /**/ + } /**/", + "a { /**/\n color: red; /**/\n} /**/\n" +); test!( removes_single_line_comment, "// a { color: red }\na {\n height: 1 1px;\n}\n", @@ -66,6 +80,16 @@ test!( "$a: foo;/* interpolation #{1 + 1} in #{$a} comments */", "/* interpolation 2 in foo comments */\n" ); +test!( + preserves_relative_whitespace, + " /*!\n * a\n */\n", + "/*!\n * a\n */\n" +); +test!( + preserves_relative_whitespace_for_each_line, + " /*!\n * a\n */\n", + "/*!\n * a\n */\n" +); test!( triple_star_in_selector, "a/***/ {x: y} b { color: red; }", @@ -140,3 +164,9 @@ test!( /**/", "a {\n color: red;\n}\na d {\n color: red;\n}\n\n/**/\nc {\n color: red;\n}\n\n/**/\n" ); +test!( + #[ignore = "we use the old form of comment writing"] + same_line_loud_comments_are_emitted_on_same_line_of_ruleset_brackets, + "a {/**/}", + "a { /**/ }\n" +); diff --git a/tests/compressed.rs b/tests/compressed.rs index de16c726..0f163ebe 100644 --- a/tests/compressed.rs +++ b/tests/compressed.rs @@ -26,6 +26,7 @@ test!( grass::Options::default().style(grass::OutputStyle::Compressed) ); test!( + #[ignore = "regress selector compression"] compresses_selector_with_newline_after_comma, "a,\nb {\n color: red;\n}\n", "a,b{color:red}", @@ -50,6 +51,7 @@ test!( grass::Options::default().style(grass::OutputStyle::Compressed) ); test!( + #[ignore = "regress not emitting the trailing semicolon here"] removes_multiline_comment_after_style, "a {\n color: red;\n /* abc */\n}\n", "a{color:red}", @@ -67,6 +69,12 @@ test!( "a{color:red}", grass::Options::default().style(grass::OutputStyle::Compressed) ); +test!( + keeps_preserved_multiline_comment_before_ruleset, + "/*! abc */a {\n color: red;\n}\n", + "/*! abc */a{color:red}", + grass::Options::default().style(grass::OutputStyle::Compressed) +); test!( removes_multiline_comment_after_ruleset, "a {\n color: red;\n}\n/* abc */", diff --git a/tests/content-exists.rs b/tests/content-exists.rs index 73d01997..ec04c27e 100644 --- a/tests/content-exists.rs +++ b/tests/content-exists.rs @@ -48,7 +48,6 @@ test!( "a {\n color: false;\n}\n" ); error!( - #[ignore = "haven't yet figured out a good way to check for whether an @content block exists"] include_empty_braces_no_args_no_at_content, "@mixin foo {\n color: content-exists();\n}\n\na {\n @include foo{};\n}\n", "Error: Mixin doesn't accept a content block." diff --git a/tests/custom-property.rs b/tests/custom-property.rs new file mode 100644 index 00000000..27cef268 --- /dev/null +++ b/tests/custom-property.rs @@ -0,0 +1,97 @@ +#[macro_use] +mod macros; + +test!( + interpolated_null_is_removed, + "a {\n --btn-font-family: #{null};\n}\n", + "a {\n --btn-font-family: ;\n}\n" +); +test!( + no_space_after_colon, + "a {\n --btn-font-family:null;\n}\n", + "a {\n --btn-font-family:null;\n}\n" +); +test!( + only_whitespace, + "a {\n --btn-font-family: ;\n}\n", + "a {\n --btn-font-family: ;\n}\n" +); +test!( + silent_comment, + "a {\n --btn-font-family: // ;\n}\n", + "a {\n --btn-font-family: // ;\n}\n" +); +test!( + interpolated_name_isnt_custom_property, + "a {\n #{--prop}:0.75;\n}\n", + "a {\n --prop: 0.75;\n}\n" +); +test!( + interpolated_name_is_custom_property_if_dashes_not_part_of_interpolation, + "a {\n --#{prop}:0.75;\n}\n", + "a {\n --prop:0.75;\n}\n" +); +test!( + prop_value_starts_with_u, + "a {\n --prop: underline;\n}\n", + "a {\n --prop: underline;\n}\n" +); +test!( + prop_value_is_url, + "a {\n --prop: url();\n}\n", + "a {\n --prop: url();\n}\n" +); +test!( + prop_value_starts_with_url, + "a {\n --prop: urlaa;\n}\n", + "a {\n --prop: urlaa;\n}\n" +); +test!( + prop_value_is_url_without_parens, + "a {\n --prop: url;\n}\n", + "a {\n --prop: url;\n}\n" +); +test!( + #[ignore = "we don't preserve newlines or indentation here"] + preserves_newlines_in_value, + "a {\n --without-semicolon: {\n a: b\n }\n}\n", + "a {\n --without-semicolon: {\n a: b\n } ;\n}\n" +); +error!( + nothing_after_colon, + "a {\n --btn-font-family:;\n}\n", "Error: Expected token." +); +error!( + #[ignore = "dart-sass crashes on this input https://github.com/sass/dart-sass/issues/1857"] + child_in_declaration_block_is_custom_property, + "a { + color: { + --foo: bar; + } + }", + "" +); +error!( + // NOTE: https://github.com/sass/dart-sass/issues/1857 + child_in_declaration_block_is_declaration_block_with_child_custom_property, + "a { + color: { + --a: { + foo: bar; + } + } + }", + r#"Error: Declarations whose names begin with "--" may not be nested"# +); +error!( + // NOTE: https://github.com/sass/dart-sass/issues/1857 + custom_property_double_nested_custom_property_with_non_string_value_before_decl_block, + "a { + color: { + --a: 2 { + --foo: bar; + } + } + }", + r#"Error: Declarations whose names begin with "--" may not be nested"# +); diff --git a/tests/debug.rs b/tests/debug.rs new file mode 100644 index 00000000..883bbf96 --- /dev/null +++ b/tests/debug.rs @@ -0,0 +1,5 @@ +#[macro_use] +mod macros; + +test!(simple_debug, "@debug 2", ""); +test!(simple_debug_with_semicolon, "@debug 2;", ""); diff --git a/tests/division.rs b/tests/division.rs index 44c28a91..1131b60d 100644 --- a/tests/division.rs +++ b/tests/division.rs @@ -71,6 +71,26 @@ test!( "a {\n color: null / 1;\n}\n", "a {\n color: /1;\n}\n" ); +test!( + null_div_named_color, + "a {\n color: null / red;\n}\n", + "a {\n color: /red;\n}\n" +); +test!( + null_div_hex_color, + "a {\n color: null / #f0f0f0;\n}\n", + "a {\n color: /#f0f0f0;\n}\n" +); +test!( + named_color_div_null, + "a {\n color: red / null;\n}\n", + "a {\n color: red/;\n}\n" +); +test!( + hex_color_div_null, + "a {\n color: #f0f0f0 / null;\n}\n", + "a {\n color: #f0f0f0/;\n}\n" +); test!( null_div_dblquoted_string, "a {\n color: null / \"foo\";\n}\n", @@ -162,6 +182,11 @@ test!( "a {\n color: 1 / 3 / 4;\n}\n", "a {\n color: 1/3/4;\n}\n" ); +test!( + long_as_slash_chain, + "a {\n color: 1/2/3/4/5/6/7/8/9;\n}\n", + "a {\n color: 1/2/3/4/5/6/7/8/9;\n}\n" +); test!( does_not_eval_chained_binop_one_not_division, "a {\n color: 1 + 3 / 4;\n}\n", @@ -172,3 +197,85 @@ test!( "a {\n color: (0 / 0);\n}\n", "a {\n color: NaN;\n}\n" ); +test!( + divide_two_calculations, + "a {\n color: (calc(1rem + 1px) / calc(1rem + 1px));\n}\n", + "a {\n color: calc(1rem + 1px)/calc(1rem + 1px);\n}\n" +); +test!( + num_div_calculation, + "a {\n color: (1 / calc(1rem + 1px));\n}\n", + "a {\n color: 1/calc(1rem + 1px);\n}\n" +); +test!( + calculation_div_null, + "a {\n color: (calc(1rem + 1px) / null);\n}\n", + "a {\n color: calc(1rem + 1px)/;\n}\n" +); +test!( + calculation_div_dbl_quoted_string, + "a {\n color: (calc(1rem + 1px) / \"foo\");\n}\n", + "a {\n color: calc(1rem + 1px)/\"foo\";\n}\n" +); +test!( + calculation_div_sgl_quoted_string, + "a {\n color: (calc(1rem + 1px) / 'foo');\n}\n", + "a {\n color: calc(1rem + 1px)/\"foo\";\n}\n" +); +test!( + three_chain_ending_in_string_is_not_evaled, + "a {\n color: 1 / 2 / foo();\n}\n", + "a {\n color: 1/2/foo();\n}\n" +); +test!( + evaluates_variable_in_each, + "$x: a 3/4 b; + + a { + @each $elem in $x { + color: $elem; + } + }", + "a {\n color: a;\n color: 0.75;\n color: b;\n}\n" +); +test!( + evaluates_multiple_variables_in_each, + "$x: a 3/4; + + a { + + @each $a, + $b in $x { + color: $a; + } + }", + "a {\n color: a;\n color: 0.75;\n}\n" +); +test!( + not_evaluated_for_variable_as_map_value_in_list, + "$a: 1 2/3 4; + + a { + color: inspect((a: $a)) + }", + "a {\n color: (a: 1 2/3 4);\n}\n" +); +test!( + is_evaluated_for_variable_as_map_value_alone, + "$a: 2/3; + + a { + color: inspect((a: $a)) + }", + "a {\n color: (a: 0.6666666667);\n}\n" +); +test!( + quoted_string_div_calculation, + "a {\n color: \"\" / calc(1vh + 1px);\n}\n", + "a {\n color: \"\"/calc(1vh + 1px);\n}\n" +); +test!( + unquoted_string_div_calculation, + "a {\n color: foo / calc(1vh + 1px);\n}\n", + "a {\n color: foo/calc(1vh + 1px);\n}\n" +); diff --git a/tests/equality.rs b/tests/equality.rs index f91625df..4b4e6f40 100644 --- a/tests/equality.rs +++ b/tests/equality.rs @@ -176,6 +176,16 @@ test!( "a {\n color: (a: b) != (a: b, c: d);\n}\n", "a {\n color: true;\n}\n" ); +test!( + eq_does_unit_conversion, + "a {\n color: 1in==2.54cm;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + ne_does_unit_conversion, + "a {\n color: 1in!=2.54cm;\n}\n", + "a {\n color: false;\n}\n" +); test!( arglist_unquoted_string_eq, "@function foo($a...) { diff --git a/tests/error.rs b/tests/error.rs index fb7bb403..36e6852f 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -83,7 +83,6 @@ error!(toplevel_comma, "a {},", "Error: expected \"{\"."); error!(toplevel_exclamation_alone, "!", "Error: expected \"}\"."); error!(toplevel_exclamation, "! {}", "Error: expected \"}\"."); error!(toplevel_backtick, "` {}", "Error: expected selector."); -// note that the message dart-sass gives is: `Error: expected "}".` error!( toplevel_open_curly_brace, "{ {color: red;}", "Error: expected \"}\"." @@ -99,10 +98,10 @@ error!( "a {color:,red;}", "Error: Expected expression." ); // dart-sass gives `Error: expected "{".` -error!(nothing_after_hyphen, "a {-}", "Error: Expected identifier."); +error!(nothing_after_hyphen, "a {-}", "Error: expected \"{\"."); error!( nothing_after_hyphen_variable, - "a {$-", "Error: expected \":\"." + "a {$-", "Error: Expected identifier." ); error!( closing_brace_after_hyphen_variable, @@ -125,17 +124,14 @@ error!( "#{", "Error: Expected expression." ); error!(toplevel_hash, "#", "Error: expected \"{\"."); -error!( - #[ignore = "we use closing brace to end scope"] - toplevel_closing_brace, - "}", "Error: unmatched \"}\"." -); +error!(toplevel_closing_brace, "}", "Error: unmatched \"}\"."); error!(toplevel_at, "@", "Error: Expected identifier."); error!( toplevel_ampersand, "& {}", "Error: Top-level selectors may not contain the parent selector \"&\"." ); -error!(toplevel_backslash, "\\", "Error: expected \"{\"."); +// note: dart-sass gives error "Expected escape sequence." +error!(toplevel_backslash, "\\", "Error: Expected expression."); error!(toplevel_var_no_colon, "$r", "Error: expected \":\"."); error!(bar_in_value, "a {color: a|b;}", "Error: expected \";\"."); error!( @@ -152,7 +148,7 @@ error!( ); error!( operator_ne, - "a {color: 5 - !=;}", "Error: Expected expression." + "a {color: 5 - !=;}", "Error: Expected \"important\"." ); error!( operator_gt, @@ -224,10 +220,7 @@ error!( empty_style_value_semicolon, "a {color:;}", "Error: Expected expression." ); -error!( - ident_colon_closing_brace, - "r:}", "Error: Expected expression." -); +error!(ident_colon_closing_brace, "r:}", "Error: expected \"{\"."); error!(dollar_sign_alone, "$", "Error: Expected identifier."); error!( nothing_after_dbl_quote, @@ -238,11 +231,16 @@ error!( invalid_binop_in_list, "a {color: foo % bar, baz;}", "Error: Undefined operation \"foo % bar\"." ); +// note: dart-sass has error "Expected identifier." error!( improperly_terminated_nested_style, - "a {foo: {bar: red", "Error: Expected identifier." + "a {foo: {bar: red", "Error: expected \"}\"." +); +error!(toplevel_nullbyte, "\u{0}", "Error: expected \"{\"."); +error!( + toplevel_nullbyte_with_braces, + "\u{0} {}", "Error: expected selector." ); -error!(toplevel_nullbyte, "\u{0}", "Error: expected selector."); error!( double_escaped_bang_at_toplevel, "\\!\\!", "Error: expected \"{\"." @@ -255,3 +253,23 @@ error!( unclosed_bracketed_list, "a { color: [a", "Error: expected \"]\"." ); +error!( + nothing_after_backslash_in_possible_style, + "a {a \\", "Error: expected more input." +); +error!( + nothing_after_bang_in_variable_decl, + "$foo: !", "Error: Expected \"important\"." +); +error!( + nothing_after_dot_in_value, + "a { color: .", "Error: Expected digit." +); +error!( + nothing_after_dot_in_value_preceded_by_plus_sign, + "a { color: +.", "Error: Expected digit." +); +error!( + nothing_after_dot_in_value_preceded_by_minus_sign, + "a { color: -.", "Error: Expected digit." +); diff --git a/tests/extend.rs b/tests/extend.rs index 42197cae..13bc1b0a 100644 --- a/tests/extend.rs +++ b/tests/extend.rs @@ -1393,7 +1393,6 @@ test!( "@media screen {\n @unknown {\n .foo, .bar {\n a: b;\n }\n }\n}\n" ); test!( - #[ignore = "media queries are not yet parsed correctly"] extend_within_separate_media_queries, "@media screen {.foo {a: b}} @media screen {.bar {@extend .foo}} @@ -1401,7 +1400,6 @@ test!( "@media screen {\n .foo, .bar {\n a: b;\n }\n}\n" ); test!( - #[ignore = "media queries are not yet parsed correctly"] extend_within_separate_unknown_at_rules, "@unknown {.foo {a: b}} @unknown {.bar {@extend .foo}} @@ -1409,7 +1407,6 @@ test!( "@unknown {\n .foo, .bar {\n a: b;\n }\n}\n@unknown {}\n" ); test!( - #[ignore = "media queries are not yet parsed correctly"] extend_within_separate_nested_at_rules, "@media screen {@flooblehoof {.foo {a: b}}} @media screen {@flooblehoof {.bar {@extend .foo}}}", @@ -1561,7 +1558,6 @@ test!( ".parent1 .child {\n a: b;\n}\n" ); test!( - #[ignore = "media queries are not yet parsed correctly"] extend_inside_double_nested_media, "@media all { @media (orientation: landscape) { @@ -1741,14 +1737,13 @@ test!( ":not(.c):not(.a):not(.d):not(.b) {\n a: b;\n}\n" ); test!( - #[ignore = "media queries are not yet parsed correctly"] does_not_move_page_block_in_media, "@media screen { a { x:y; } @page {} } ", - "@media screen {\n a {\n x: y;\n }\n\n @page {}\n}\n" + "@media screen {\n a {\n x: y;\n }\n @page {}\n}\n" ); test!( escaped_selector, @@ -1921,7 +1916,11 @@ test!( }", "c b {\n color: red;\n}\n" ); - +test!( + unification_subselector_of_target_where, + r#"a {b: selector-extend(".c:where(d)", ":where(d)", "d.e")}"#, + "a {\n b: .c:where(d);\n}\n" +); error!( extend_optional_keyword_not_complete, "a { @@ -1936,6 +1935,37 @@ error!( }", "Error: Parent selectors aren't allowed here." ); +error!( + #[ignore = "we do not currently respect this"] + extend_across_media_boundary, + "a { + display: none; + } + + @media only screen and (min-width:300px) { + a { + @extend a; + } + }", + "Error: You may not @extend selectors across media queries." +); +error!( + #[ignore = "we do not error for this"] + extend_target_does_not_exist, + "a { + @extend dne; + }", + "Error: The target selector was not found." +); +error!( + #[ignore = "crash"] + extends_self_is_has_invalid_combinator, + "a :is(#a, >) { + @extend a + }", + "" +); // todo: extend_loop (massive test) // todo: extend tests in folders +// todo: copy all :where extend tests, https://github.com/sass/sass-spec/pull/1783/files diff --git a/tests/for.rs b/tests/for.rs index 00620060..7d2fea9a 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -194,14 +194,6 @@ error!( to_value_is_non_numeric, "@for $i from 0 to red {}", "Error: red is not a number." ); -error!( - through_i32_max, - "@for $i from 0 through 2147483647 {}", "Error: 2147483647 is not an int." -); -error!( - from_i32_max, - "@for $i from 2147483647 through 0 {}", "Error: 2147483647 is not an int." -); error!( from_nan, "@for $i from (0/0) through 0 {}", "Error: NaN is not an int." @@ -210,7 +202,34 @@ error!( to_nan, "@for $i from 0 through (0/0) {}", "Error: NaN is not an int." ); -error!( +test!( to_and_from_i32_min, - "@for $i from -2147483648 through -2147483648 {}", "Error: -2147483648 is not an int." + "@for $i from -2147483648 through -2147483648 {}", + "" +); +test!( + to_and_from_comparable_units, + "@for $i from 1px to (3px * 1in) / 1in { + a { + color: $i; + } + }", + "a {\n color: 1px;\n}\n\na {\n color: 2px;\n}\n" +); +error!( + to_and_from_non_comparable_units, + "@for $i from 1px to 2vh { + a { + color: $i; + } + }", + "Error: to and from values have incompatible units" +); +error!( + invalid_escape_sequence_in_declaration, + "@for $i from 0 \\110000 o 2 {}", "Error: Invalid Unicode code point." +); +error!( + invalid_keyword_after_first_number, + "@for $i from 1 FOO 3 {}", "Error: Expected \"to\" or \"through\"." ); diff --git a/tests/forward.rs b/tests/forward.rs new file mode 100644 index 00000000..d35b8fa5 --- /dev/null +++ b/tests/forward.rs @@ -0,0 +1,210 @@ +use std::io::Write; + +use macros::TestFs; + +#[macro_use] +mod macros; + +#[test] +fn basic_forward() { + let input = r#" + @use "basic_forward__b"; + + a { + color: basic_forward__b.$a; + } + "#; + tempfile!("basic_forward__b.scss", r#"@forward "basic_forward__a";"#); + tempfile!("basic_forward__a.scss", r#"$a: red;"#); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn basic_forward_with_configuration() { + let input = r#" + @use "basic_forward_with_configuration__b"; + + a { + color: basic_forward_with_configuration__b.$a; + } + "#; + tempfile!( + "basic_forward_with_configuration__b.scss", + r#"@forward "basic_forward_with_configuration__a" with ($a: green);"# + ); + tempfile!( + "basic_forward_with_configuration__a.scss", + r#"$a: red !default;"# + ); + assert_eq!( + "a {\n color: green;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn basic_forward_with_configuration_no_default_error() { + let input = r#" + @use "basic_forward_with_configuration_no_default_error__b"; + + a { + color: basic_forward_with_configuration_no_default_error__b.$a; + } + "#; + tempfile!( + "basic_forward_with_configuration_no_default_error__b.scss", + r#"@forward "basic_forward_with_configuration_no_default_error__a" with ($a: green);"# + ); + tempfile!( + "basic_forward_with_configuration_no_default_error__a.scss", + r#"$a: red;"# + ); + assert_err!( + "Error: This variable was not declared with !default in the @used module.", + input + ); +} + +// todo: same test for fns and mixins? +#[test] +fn can_redeclare_forwarded_upstream_vars() { + let input = r#" + @use "can_redeclare_forwarded_upstream_vars__a" as a; + @use "can_redeclare_forwarded_upstream_vars__b" as b; + + a { + color: a.$a; + color: b.$a; + } + "#; + tempfile!( + "can_redeclare_forwarded_upstream_vars__b.scss", + r#" + @forward "can_redeclare_forwarded_upstream_vars__a"; + + $a: midstream; + "# + ); + tempfile!( + "can_redeclare_forwarded_upstream_vars__a.scss", + r#"$a: upstream;"# + ); + assert_eq!( + "a {\n color: upstream;\n color: midstream;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn through_forward_with_as() { + let mut fs = TestFs::new(); + + fs.add_file( + "_downstream.scss", + r#"@forward "midstream" with ($b-a: configured);"#, + ); + fs.add_file("_midstream.scss", r#"@forward "upstream" as b-*;"#); + fs.add_file( + "_upstream.scss", + r#" + $a: original !default; + c {d: $a} + "#, + ); + + let input = r#"@use "downstream";"#; + + assert_eq!( + "c {\n d: configured;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} +#[test] +fn through_forward_with_unconfigured() { + let mut fs = TestFs::new(); + + fs.add_file( + "_downstream.scss", + r#"@forward "midstream" with ($a: from downstream);"#, + ); + fs.add_file( + "_midstream.scss", + r#"@forward "upstream" with ($b: from midstream !default);"#, + ); + fs.add_file( + "_upstream.scss", + r#" + $a: from upstream !default; + $b: from upstream !default; + c { + a: $a; + b: $b; + } + "#, + ); + + let input = r#"@use "downstream";"#; + + assert_eq!( + "c {\n a: from downstream;\n b: from midstream;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn member_visibility_variable_declaration() { + let mut fs = TestFs::new(); + + fs.add_file("_midstream.scss", r#"@forward "upstream" hide d;"#); + fs.add_file( + "_upstream.scss", + r#" + $a: old value; + + @function get-a() {@return $a} + "#, + ); + + let input = r#" + @use "midstream"; + + midstream.$a: new value; + + b {c: midstream.get-a()}; + "#; + + assert_eq!( + "b {\n c: new value;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +#[ignore = "forward is still WIP"] +fn member_import_precedence_top_level() { + let mut fs = TestFs::new(); + + fs.add_file("_midstream.scss", r#"@forward "upstream";"#); + fs.add_file( + "_upstream.scss", + r#" + $a: in-upstream; + "#, + ); + + let input = r#" + $a: in-input; + + @import "midstream"; + + b {c: $a} + "#; + + assert_eq!( + "b {\n c: in-upstream;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} diff --git a/tests/functions.rs b/tests/functions.rs index f8498f37..92366b88 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -314,7 +314,7 @@ error!( a { color: foo(nul); }", - "Error: Functions can only contain variable declarations and control directives." + "Error: expected \".\"." ); error!( pass_one_arg_to_fn_that_accepts_zero, @@ -338,6 +338,22 @@ error!( }", "Error: Only 1 argument allowed, but 2 were passed." ); +error!( + declaration_inside_function, + "@function foo() { + opacity: 1; + @return 2; + }", + "Error: @function rules may not contain declarations." +); +error!( + style_rule_inside_function, + "@function foo() { + a {} + @return 2; + }", + "Error: @function rules may not contain style rules." +); test!( allows_multiline_comment, "@function foo($a) { @@ -361,3 +377,37 @@ test!( }", "a {\n color: red;\n}\n" ); +test!( + return_inside_each, + "@function foo() { + @each $i in 0 { + @return $i; + } + } + + a { + color: foo(); + }", + "a {\n color: 0;\n}\n" +); +test!( + recursive_function_cannot_modify_scope_of_calling_function, + "@function with-local-variable($recurse) { + $var: before; + + @if ($recurse) { + $a: with-local-variable($recurse: false); + } + + $ret: $var; + $var: after; + @return $ret; + } + + a { + color: with-local-variable($recurse: true); + }", + "a {\n color: before;\n}\n" +); + +// todo: return inside if, return inside while, return inside for diff --git a/tests/if.rs b/tests/if.rs index 71d4a46f..483bab25 100644 --- a/tests/if.rs +++ b/tests/if.rs @@ -217,10 +217,7 @@ error!( ); error!(unclosed_dbl_quote, "@if true \" {}", "Error: Expected \"."); error!(unclosed_sgl_quote, "@if true ' {}", "Error: Expected '."); -error!( - unclosed_call_args, - "@if a({}", "Error: Expected expression." -); +error!(unclosed_call_args, "@if a({}", "Error: expected \")\"."); error!(nothing_after_div, "@if a/", "Error: Expected expression."); error!(multiline_error, "@if \"\n\"{}", "Error: Expected \"."); error!( @@ -260,3 +257,14 @@ test!( }", "/**/\n" ); +test!( + elseif_is_parsed_as_else_if, + r"@if false {} + + @elseif true { + a { + color: red; + } + }", + "a {\n color: red;\n}\n" +); diff --git a/tests/important.rs b/tests/important.rs index a361295d..a92aa2cc 100644 --- a/tests/important.rs +++ b/tests/important.rs @@ -31,3 +31,5 @@ test!( "a {\n color: ! important;\n}\n", "a {\n color: !important;\n}\n" ); + +// todo: loud comment between !<>i, silent comment between !<>i diff --git a/tests/imports.rs b/tests/imports.rs index 848e37c3..057b0d75 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -1,4 +1,6 @@ -use std::io::Write; +use std::{io::Write, path::Path}; + +use macros::TestFs; #[macro_use] mod macros; @@ -24,11 +26,20 @@ fn null_fs_cannot_import() { #[test] fn imports_variable() { - let input = "@import \"imports_variable\";\na {\n color: $a;\n}"; - tempfile!("imports_variable", "$a: red;"); + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"$a: red;"#); + + let input = r#" + @import "a"; + a { + color: $a; + } + "#; + assert_eq!( "a {\n color: red;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } @@ -44,53 +55,81 @@ fn import_no_semicolon() { #[test] fn import_no_quotes() { let input = "@import import_no_quotes"; - tempfile!("import_no_quotes", "$a: red;"); assert_err!("Error: Expected string.", input); } #[test] fn single_quotes_import() { - let input = "@import 'single_quotes_import';\na {\n color: $a;\n}"; - tempfile!("single_quotes_import", "$a: red;"); + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"$a: red;"#); + + let input = r#" + @import 'a'; + a { + color: $a; + } + "#; + assert_eq!( "a {\n color: red;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } #[test] fn comma_separated_import() { - let input = "@import 'comma_separated_import_first', 'comma_separated_import_second';\na {\n color: $a;\n}"; - tempfile!("comma_separated_import_first", "$a: red;"); - tempfile!("comma_separated_import_second", "p { color: blue; }"); + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"$a: red"#); + fs.add_file("b.scss", r#"p { color: blue; }"#); + + let input = r#" + @import 'a', 'b'; + + a { + color: $a; + } + "#; + assert_eq!( "p {\n color: blue;\n}\n\na {\n color: red;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } #[test] fn comma_separated_import_order() { - let input = - "@import 'comma_separated_import_order1', 'comma_separated_import_order2', url(third);"; - tempfile!("comma_separated_import_order1", "p { color: red; }"); - tempfile!("comma_separated_import_order2", "p { color: blue; }"); + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"p { color: red; }"#); + fs.add_file("b.scss", r#"p { color: blue; }"#); + + let input = r#" + @import "a", "b", url(third); + "#; + assert_eq!( "@import url(third);\np {\n color: red;\n}\n\np {\n color: blue;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } #[test] fn comma_separated_import_order_css() { - let input = - "@import 'comma_separated_import_order1.css', 'comma_separated_import_order_css', url(third);"; - tempfile!("comma_separated_import_order1.css", "p { color: red; }"); - tempfile!("comma_separated_import_order_css", "p { color: blue; }"); + let mut fs = TestFs::new(); + + fs.add_file("a.css", r#"p { color: red; }"#); + fs.add_file("b.css", r#"p { color: blue; }"#); + + let input = r#" + @import "a.css", "b", url(third); + "#; + assert_eq!( - "@import \"comma_separated_import_order1.css\";\n@import url(third);\np {\n color: blue;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + "@import \"a.css\";\n@import url(third);\np {\n color: blue;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } @@ -117,58 +156,264 @@ fn basic_load_path() { ); } +#[test] +fn load_path_same_directory() { + tempfile!( + "load_path_same_directory__a.scss", + "@import \"dir-load_path_same_directory__a/load_path_same_directory__b\";\na {\n color: $a;\n}", + dir = "dir-load_path_same_directory__a" + ); + tempfile!( + "load_path_same_directory__b.scss", + "$a: red;", + dir = "dir-load_path_same_directory__a" + ); + + assert_eq!( + "a {\n color: red;\n}\n", + grass::from_path( + "dir-load_path_same_directory__a/load_path_same_directory__a.scss", + &grass::Options::default().load_path(std::path::Path::new(".")) + ) + .unwrap() + ); +} + #[test] fn comma_separated_import_trailing() { - let input = - "@import 'comma_separated_import_trailing1', 'comma_separated_import_trailing2', url(third),,,,,,,,;"; - tempfile!("comma_separated_import_trailing1", "p { color: red; }"); - tempfile!("comma_separated_import_trailing2", "p { color: blue; }"); + let mut fs = TestFs::new(); - match grass::from_string(input.to_string(), &grass::Options::default()) { - Ok(..) => panic!("did not fail"), - Err(e) => assert_eq!( - "Error: Expected expression.", - e.to_string() - .chars() - .take_while(|c| *c != '\n') - .collect::() - .as_str() - ), - } + fs.add_file("a.scss", r#"p { color: red; }"#); + fs.add_file("b.scss", r#"p { color: blue; }"#); + + let input = r#" + @import "a", "b", url(third),,,,,,,,; + "#; + + assert_err!("Error: Expected string.", input); } #[test] fn finds_name_scss() { - let input = "@import \"finds_name_scss\";\na {\n color: $a;\n}"; - tempfile!("finds_name_scss.scss", "$a: red;"); + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"$a: red;"#); + + let input = r#" + @import "a"; + a { + color: $a; + } + "#; + assert_eq!( "a {\n color: red;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } #[test] fn finds_underscore_name_scss() { - let input = "@import \"finds_underscore_name_scss\";\na {\n color: $a;\n}"; - tempfile!("_finds_underscore_name_scss.scss", "$a: red;"); + let mut fs = TestFs::new(); + fs.add_file("_a.scss", r#"$a: red;"#); + + let input = r#" + @import "a"; + a { + color: $a; + } + "#; + assert_eq!( "a {\n color: red;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) ); } #[test] fn chained_imports() { - let input = "@import \"chained_imports__a\";\na {\n color: $a;\n}"; - tempfile!("chained_imports__a.scss", "@import \"chained_imports__b\";"); - tempfile!("chained_imports__b.scss", "@import \"chained_imports__c\";"); - tempfile!("chained_imports__c.scss", "$a: red;"); + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"@import "b";"#); + fs.add_file("b.scss", r#"@import "c";"#); + fs.add_file("c.scss", r#"$a: red;"#); + + let input = r#" + @import "a"; + a { + color: $a; + } + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn imports_plain_css() { + let mut fs = TestFs::new(); + + fs.add_file("a.css", r#"a { color: red; }"#); + + let input = r#" + @import "a"; + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn imports_import_only_scss() { + let mut fs = TestFs::new(); + + fs.add_file("a.import.scss", r#"a { color: red; }"#); + + let input = r#" + @import "a"; + "#; + assert_eq!( "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn imports_absolute_scss() { + let mut fs = TestFs::new(); + + fs.add_file("/foo/a.scss", r#"a { color: red; }"#); + + let input = r#" + @import "/foo/a"; + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn imports_explicit_file_extension() { + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"a { color: red; }"#); + + let input = r#" + @import "a.scss"; + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn potentially_conflicting_directory_and_file() { + tempfile!( + "index.scss", + "$a: wrong;", + dir = "potentially_conflicting_directory_and_file" + ); + tempfile!( + "_potentially_conflicting_directory_and_file.scss", + "$a: right;" + ); + + let input = r#" + @import "potentially_conflicting_directory_and_file"; + a { + color: $a; + } + "#; + + assert_eq!( + "a {\n color: right;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn finds_index_file_no_underscore() { + tempfile!( + "index.scss", + "$a: right;", + dir = "finds_index_file_no_underscore" + ); + + let input = r#" + @import "finds_index_file_no_underscore"; + a { + color: $a; + } + "#; + + assert_eq!( + "a {\n color: right;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn finds_index_file_with_underscore() { + tempfile!( + "_index.scss", + "$a: right;", + dir = "finds_index_file_with_underscore" + ); + + let input = r#" + @import "finds_index_file_with_underscore"; + a { + color: $a; + } + "#; + + assert_eq!( + "a {\n color: right;\n}\n", &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) ); } +#[test] +fn potentially_conflicting_directory_and_file_from_load_path() { + tempfile!( + "_potentially_conflicting_directory_and_file_from_load_path.scss", + "$a: right;", + dir = "potentially_conflicting_directory_and_file_from_load_path__a" + ); + tempfile!( + "index.scss", + "$a: wrong;", + dir = "potentially_conflicting_directory_and_file_from_load_path__a/potentially_conflicting_directory_and_file_from_load_path" + ); + + let input = r#" + @import "potentially_conflicting_directory_and_file_from_load_path"; + a { + color: $a; + } + "#; + + assert_eq!( + "a {\n color: right;\n}\n", + &grass::from_string( + input.to_string(), + &grass::Options::default().load_path(&Path::new( + "potentially_conflicting_directory_and_file_from_load_path__a" + )) + ) + .expect(input) + ); +} + #[test] fn chained_imports_in_directory() { let input = "@import \"chained_imports_in_directory__a\";\na {\n color: $a;\n}"; @@ -189,8 +434,9 @@ fn chained_imports_in_directory() { } error!( + // note: dart-sass error is "expected more input." missing_input_after_import, - "@import", "Error: expected more input." + "@import", "Error: Expected string." ); error!( import_unquoted_http, @@ -241,6 +487,11 @@ test!( "@import \"//fonts.googleapis.com/css?family=Droid+Sans\";", "@import \"//fonts.googleapis.com/css?family=Droid+Sans\";\n" ); +test!( + plain_css_retains_backslash_for_escaped_space, + r#"@import "hux\ bux.css";"#, + "@import \"hux\\ bux.css\";\n" +); test!( plain_css_is_moved_to_top_of_file, "a { @@ -250,7 +501,16 @@ test!( @import url(\"foo.css\");", "@import url(\"foo.css\");\na {\n color: red;\n}\n" ); +test!( + many_import_conditions, + r#"@import "a" b c d(e) supports(f: g) h i j(k) l m (n: o), (p: q);"#, + "@import \"a\" b c d(e) supports(f: g) h i j(k) l m (n: o), (p: q);\n" +); +error!(unclosed_single_quote, r#"@import '"#, "Error: Expected '."); +error!(unclosed_double_quote, r#"@import ""#, "Error: Expected \"."); // todo: edge case tests for plain css imports moved to top // todo: test for calling paths, e.g. `grass b\index.scss` // todo: test for absolute paths (how?) +// todo: test for @import accessing things declared beforehand +// e.g. b { @import } | $a: red; @import diff --git a/tests/interpolation.rs b/tests/interpolation.rs index 26fa2e57..b271723b 100644 --- a/tests/interpolation.rs +++ b/tests/interpolation.rs @@ -72,7 +72,6 @@ test!( "a {\n color: foo(a, 3, c);\n}\n" ); test!( - #[ignore = "we evaluate interpolation eagerly"] interpolated_builtin_fn, "a {\n color: uni#{t}less(1px);\n}\n", "a {\n color: unitless(1px);\n}\n" diff --git a/tests/keyframes.rs b/tests/keyframes.rs index 3632692d..30fb9647 100644 --- a/tests/keyframes.rs +++ b/tests/keyframes.rs @@ -127,6 +127,33 @@ test!( }", "@-webkit-keyframes foo {\n 0% {\n color: red;\n }\n}\n" ); +test!( + keyframes_percent_has_e, + "@keyframes foo { + 1e2% { + color: red; + } + }", + "@keyframes foo {\n 1e2% {\n color: red;\n }\n}\n" +); +test!( + keyframes_percent_has_plus_e, + "@keyframes foo { + 1e+2% { + color: red; + } + }", + "@keyframes foo {\n 1e+2% {\n color: red;\n }\n}\n" +); +test!( + keyframes_percent_has_negative_e, + "@keyframes foo { + 1e-2% { + color: red; + } + }", + "@keyframes foo {\n 1e-2% {\n color: red;\n }\n}\n" +); test!( keyframes_allow_decimal_selector, "@keyframes foo { @@ -161,17 +188,131 @@ error!( color: red; } }", - "Error: Expected \"to\" or \"from\"." + "Error: Expected number." ); error!( keyframes_nothing_after_forward_slash_in_selector, - "@keyframes foo { a/", "Error: Expected selector." + "@keyframes foo { a/", "Error: expected \"{\"." ); error!( keyframes_no_ident_after_forward_slash_in_selector, - "@keyframes foo { a/ {} }", "Error: expected selector." + "@keyframes foo { a/ {} }", "Error: Expected \"to\" or \"from\"." ); error!( keyframes_nothing_after_selector, "@keyframes foo { a", "Error: expected \"{\"." ); +test!( + e_alone, + "@keyframes foo { + 1e3% { + color: red; + } + }", + "@keyframes foo {\n 1e3% {\n color: red;\n }\n}\n" +); +test!( + e_with_plus, + "@keyframes foo { + 1e+3% { + color: red; + } + }", + "@keyframes foo {\n 1e+3% {\n color: red;\n }\n}\n" +); +test!( + e_with_minus, + "@keyframes foo { + 1e-3% { + color: red; + } + }", + "@keyframes foo {\n 1e-3% {\n color: red;\n }\n}\n" +); +test!( + e_with_decimal_plus, + "@keyframes foo { + 1.5e+3% { + color: red; + } + }", + "@keyframes foo {\n 1.5e+3% {\n color: red;\n }\n}\n" +); +test!( + e_with_decimal_no_number_after_decimal, + "@keyframes foo { + 1.e3% { + color: red; + } + }", + "@keyframes foo {\n 1.e3% {\n color: red;\n }\n}\n" +); +test!( + uppercase_e, + "@keyframes foo { + 1E3% { + color: red; + } + }", + "@keyframes foo {\n 1e3% {\n color: red;\n }\n}\n" +); +test!( + escaped_e, + "@keyframes foo { + 1\\65 3% { + color: red; + } + }", + "@keyframes foo {\n 1e3% {\n color: red;\n }\n}\n" +); +test!( + uppercase_escaped_e, + "@keyframes foo { + 1\\45 3% { + color: red; + } + }", + "@keyframes foo {\n 1e3% {\n color: red;\n }\n}\n" +); +test!( + style_rule_before_keyframes, + "a { + color: red; + } + + @keyframes spinner-border { + to { + color: red; + } + }", + "a {\n color: red;\n}\n\n@keyframes spinner-border {\n to {\n color: red;\n }\n}\n" +); +test!( + style_rule_after_keyframes, + "@keyframes spinner-border { + to { + color: red; + } + } + + a { + color: red; + }", + "@keyframes spinner-border {\n to {\n color: red;\n }\n}\na {\n color: red;\n}\n" +); +error!( + invalid_escape_in_place_of_e, + "@keyframes foo { + 1\\110000 3% { + color: red; + } + }", + r#"Error: expected "%"."# +); + +// todo: span for this +// @keyframes foo { +// 1\1100000000000000 3% { +// // color: \110000; +// } +// } diff --git a/tests/keywords.rs b/tests/keywords.rs new file mode 100644 index 00000000..a4b93561 --- /dev/null +++ b/tests/keywords.rs @@ -0,0 +1,47 @@ +#[macro_use] +mod macros; + +test!( + basic_keywords, + "@function foo($args...) { + @return inspect(keywords($args)); + } + a { + color: foo($a: 1, $b: 2, $c: 3); + }", + "a {\n color: (a: 1, b: 2, c: 3);\n}\n" +); +test!( + access_keywords_in_variable, + "@function foo($args...) { + $a: keywords($args); + @return 2; + } + a { + color: foo($a: 1, $b: 2, $c: 3); + }", + "a {\n color: 2;\n}\n" +); +error!( + keywords_not_accessed, + "@function foo($args...) { + @return 2; + } + a { + color: foo($a: 1, $b: 2, $c: 3); + }", + "Error: No arguments named $a, $b or $c." +); +test!( + keywords_in_meta_module, + r#" + @use "sass:meta"; + @function foo($args...) { + @return inspect(meta.keywords($args)); + } + + a { + color: foo($a: 1, $b: 2, $c: 3); + }"#, + "a {\n color: (a: 1, b: 2, c: 3);\n}\n" +); diff --git a/tests/list.rs b/tests/list.rs index ed7bb6cd..51fc5354 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -292,6 +292,11 @@ test!( "a {\n color: [1,];\n}\n", "a {\n color: [1];\n}\n" ); +test!( + space_separated_list_of_bracketed_lists, + "a {\n color: [[]] [[]] [[]];\n}\n", + "a {\n color: [[]] [[]] [[]];\n}\n" +); test!( null_values_in_list_ommitted, "a {\n color: 1, null, null;\n}\n", @@ -372,6 +377,16 @@ test!( "a {\n color: [ /**/ ];\n}\n", "a {\n color: [];\n}\n" ); +test!( + parens_in_space_separated_list, + "a {\n color: foo () bar;\n}\n", + "a {\n color: foo bar;\n}\n" +); +test!( + parens_in_comma_separated_list, + "a {\n color: foo, (), bar;\n}\n", + "a {\n color: foo, bar;\n}\n" +); test!( space_separated_inside_comma_separated, "$a: 1 2 3 == 1, 2, 3; @@ -387,6 +402,21 @@ test!( "a {\n color: 1 2 3 ;\n}\n", "a {\n color: 1 2 3;\n}\n" ); +test!( + bracketed_list_with_only_null_elements, + "a {\n color: [null, null, null];\n}\n", + "a {\n color: [];\n}\n" +); +test!( + bracketed_list_with_single_null_element, + "a {\n color: [null];\n}\n", + "a {\n color: [];\n}\n" +); +test!( + comma_separated_list_has_element_beginning_with_capital_A, + "a {\n color: a, A, \"Noto Color Emoji\";\n}\n", + "a {\n color: a, A, \"Noto Color Emoji\";\n}\n" +); error!( invalid_item_in_space_separated_list, "a {\n color: red color * #abc;\n}\n", "Error: Undefined operation \"color * #abc\"." diff --git a/tests/macros.rs b/tests/macros.rs index 8d8eaa58..1319d4f9 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -1,3 +1,11 @@ +use std::{ + borrow::Cow, + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use grass::Fs; + #[macro_export] macro_rules! test { (@base $( #[$attr:meta] ),*$func:ident, $input:expr, $output:expr, $options:expr) => { @@ -25,12 +33,12 @@ macro_rules! test { /// Span and scope information are not yet tested #[macro_export] macro_rules! error { - ($( #[$attr:meta] ),*$func:ident, $input:expr, $err:expr) => { + (@base $( #[$attr:meta] ),*$func:ident, $input:expr, $err:expr, $options:expr) => { $(#[$attr])* #[test] #[allow(non_snake_case)] fn $func() { - match grass::from_string($input.to_string(), &grass::Options::default()) { + match grass::from_string($input.to_string(), &$options) { Ok(..) => panic!("did not fail"), Err(e) => assert_eq!($err, e.to_string() .chars() @@ -41,6 +49,12 @@ macro_rules! error { } } }; + ($( #[$attr:meta] ),*$func:ident, $input:expr, $err:expr) => { + error!(@base $(#[$attr])* $func, $input, $err, grass::Options::default()); + }; + ($( #[$attr:meta] ),*$func:ident, $input:expr, $err:expr, $options:expr) => { + error!(@base $(#[$attr])* $func, $input, $err, $options); + }; } /// Create a temporary file with the given name @@ -61,12 +75,18 @@ macro_rules! tempfile { write!(f, "{}", $content).unwrap(); }; ($name:literal, $content:literal, dir=$dir:literal) => { - let _d = tempfile::Builder::new() - .rand_bytes(0) - .prefix("") - .suffix($dir) - .tempdir_in("") - .unwrap(); + let _d = if !std::path::Path::new($dir).is_dir() { + Some( + tempfile::Builder::new() + .rand_bytes(0) + .prefix("") + .suffix($dir) + .tempdir_in("") + .unwrap(), + ) + } else { + None + }; let mut f = tempfile::Builder::new() .rand_bytes(0) .prefix("") @@ -93,3 +113,39 @@ macro_rules! assert_err { } }; } + +/// Suitable for simple import tests. Does not properly implement path resolution -- +/// paths like `a/../b` will not work +#[derive(Debug)] +pub struct TestFs { + files: BTreeMap>, +} + +#[allow(unused)] +impl TestFs { + pub fn new() -> Self { + Self { + files: BTreeMap::new(), + } + } + + pub fn add_file(&mut self, name: &'static str, contents: &'static str) { + self.files + .insert(PathBuf::from(name), Cow::Borrowed(contents)); + } +} + +#[allow(unused)] +impl Fs for TestFs { + fn is_file(&self, path: &Path) -> bool { + self.files.contains_key(path) + } + + fn is_dir(&self, path: &Path) -> bool { + false + } + + fn read(&self, path: &Path) -> std::io::Result> { + Ok(self.files.get(path).unwrap().as_bytes().to_vec()) + } +} diff --git a/tests/math-module.rs b/tests/math-module.rs index 10d7573d..d37cdbc3 100644 --- a/tests/math-module.rs +++ b/tests/math-module.rs @@ -79,7 +79,7 @@ error!( error!( cos_non_angle, "@use 'sass:math';\na {\n color: math.cos(1px);\n}\n", - "Error: $number: Expected 1px to be an angle." + "Error: $number: Expected 1px to have an angle unit (deg, grad, rad, turn)." ); test!( cos_small_degree, @@ -124,7 +124,7 @@ test!( error!( sin_non_angle, "@use 'sass:math';\na {\n color: math.sin(1px);\n}\n", - "Error: $number: Expected 1px to be an angle." + "Error: $number: Expected 1px to have an angle unit (deg, grad, rad, turn)." ); test!( sin_small_degree, @@ -169,7 +169,7 @@ test!( error!( tan_non_angle, "@use 'sass:math';\na {\n color: math.tan(1px);\n}\n", - "Error: $number: Expected 1px to be an angle." + "Error: $number: Expected 1px to have an angle unit (deg, grad, rad, turn)." ); test!( tan_small_degree, @@ -337,7 +337,6 @@ test!( "a {\n color: NaN;\n}\n" ); test!( - #[ignore = "we do not support Infinity"] log_zero, "@use 'sass:math';\na {\n color: math.log(0);\n}\n", "a {\n color: -Infinity;\n}\n" @@ -368,7 +367,6 @@ test!( "a {\n color: NaN;\n}\n" ); test!( - #[ignore = "we do not support Infinity"] log_base_one, "@use 'sass:math';\na {\n color: math.log(2, 1);\n}\n", "a {\n color: Infinity;\n}\n" @@ -593,6 +591,11 @@ test!( "@use 'sass:math';\na {\n color: math.div(1, 2);\n}\n", "a {\n color: 0.5;\n}\n" ); +test!( + clamp_nan, + "@use 'sass:math';\na {\n color: math.clamp((0/0), 5, (0/0));\n}\n", + "a {\n color: 5;\n}\n" +); test!( div_two_strings, "@use 'sass:math';\na {\n color: math.div(\"1\",\"2\");\n}\n", @@ -607,3 +610,5 @@ test!( }", "a {\n color: 3;\n color: 3;\n}\n" ); + +// todo: atan+asin with unitful NaN diff --git a/tests/math.rs b/tests/math.rs index 10f1ea7f..04ecc992 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -11,6 +11,21 @@ test!( "a {\n color: percentage(100px / 50px);\n}\n", "a {\n color: 200%;\n}\n" ); +test!( + percentage_nan, + "a {\n color: percentage((0/0));\n}\n", + "a {\n color: NaN%;\n}\n" +); +test!( + percentage_infinity, + "a {\n color: percentage((1/0));\n}\n", + "a {\n color: Infinity%;\n}\n" +); +test!( + percentage_neg_infinity, + "a {\n color: percentage((-1/0));\n}\n", + "a {\n color: -Infinity%;\n}\n" +); test!( integer_division, "a {\n color: percentage(2);\n}\n", @@ -54,7 +69,7 @@ test!( test!( ceil_big_int, "a {\n color: ceil(1.000000000000000001);\n}\n", - "a {\n color: 2;\n}\n" + "a {\n color: 1;\n}\n" ); test!( abs_positive, @@ -106,8 +121,8 @@ test!( "a {\n color: random(1);\n}\n", "a {\n color: 1;\n}\n" ); -test!( +error!( random_limit_big_one, "a {\n color: random(1000000000000000001 - 1000000000000000000);\n}\n", - "a {\n color: 1;\n}\n" + "Error: $limit: Must be greater than 0, was 0." ); diff --git a/tests/media.rs b/tests/media.rs index 617299ad..c7d69358 100644 --- a/tests/media.rs +++ b/tests/media.rs @@ -120,7 +120,29 @@ test!( color: green; } }", - "@media print {\n a {\n color: red;\n }\n\n b {\n color: green;\n }\n}\n" + "@media print {\n a {\n color: red;\n }\n b {\n color: green;\n }\n}\n" +); +test!( + removes_media_if_all_children_are_blank, + "@media foo { + a {} + }", + "" +); +test!( + correct_order_of_children_when_merging, + "@media (foo) { + @media (bar) { + a { + color: red; + } + } + + a { + color: red; + } + }", + "@media (foo) and (bar) {\n a {\n color: red;\n }\n}\n@media (foo) {\n a {\n color: red;\n }\n}\n" ); test!( newline_emitted_before_media_when_following_ruleset, @@ -233,7 +255,37 @@ test!( "@media (max-width: 0px) {\n a {\n color: red;\n }\n}\n\na {\n color: red;\n}\n" ); test!( - #[ignore = "we move to top of media"] + nested_media_with_compatible_queries, + "@media (foo) { + @media (bar) { + a { + color: red; + } + } + }", + "@media (foo) and (bar) {\n a {\n color: red;\n }\n}\n" +); +test!( + nested_media_with_incompatible_queries, + "@media foo { + @media bar { + a { + color: red; + } + } + }", + "" +); +test!( + removes_media_if_all_children_are_placeholder, + "@media foo { + %a { + color: red; + } + }", + "" +); +test!( plain_import_inside_media_is_not_moved_to_top, r#"@media foo { a { @@ -242,9 +294,8 @@ test!( @import "foo.css"; }"#, - "@media foo {\n a {\n color: red;\n }\n\n @import \"foo.css\";\n}\n" + "@media foo {\n a {\n color: red;\n }\n @import \"foo.css\";\n}\n" ); - error!( media_feature_missing_closing_paren, "@media foo and (bar:a", "Error: expected \")\"." @@ -253,3 +304,269 @@ error!( media_feature_missing_curly_brace_after_hash, "@media foo and # {}", "Error: expected \"{\"." ); +error!( + // note: dart-sass gives error "Expected expression" + nothing_after_not_in_parens, + "@media (not", "Error: Expected whitespace." +); +error!( + nothing_after_and, + "@media foo and", "Error: Expected whitespace." +); +error!(nothing_after_or, "@media foo or", r#"Error: expected "{"."#); +error!( + no_parens_after_and, + "@media foo and bar { + a { + color: red; + } + }", + "Error: expected media condition in parentheses." +); +test!( + query_starts_with_interpolation, + "@media #{foo} { + a { + color: red; + } + }", + "@media foo {\n a {\n color: red;\n }\n}\n" +); +test!( + query_is_parens_with_comma, + "@media (foo, bar) { + a { + color: red; + } + }", + "@media (foo, bar) {\n a {\n color: red;\n }\n}\n" +); +test!( + query_is_parens_with_space_before_comma, + "@media (foo , bar) { + a { + color: red; + } + }", + "@media (foo, bar) {\n a {\n color: red;\n }\n}\n" +); +test!( + query_and_first_has_no_parens, + "@media foo and (bar) { + a { + color: red; + } + }", + "@media foo and (bar) {\n a {\n color: red;\n }\n}\n" +); +test!( + query_comma_separated_list_both_parens, + "@media (foo), (bar) { + a { + color: red; + } + }", + "@media (foo), (bar) {\n a {\n color: red;\n }\n}\n" +); +test!( + query_comma_separated_list_both_parens_space_before_paren, + "@media (foo) , (bar) { + a { + color: red; + } + }", + "@media (foo), (bar) {\n a {\n color: red;\n }\n}\n" +); +test!( + query_comma_separated_list_loud_comments, + "@media /**/foo/**/,/**/bar/**/ { + a { + color: red; + } + }", + "@media foo, bar {\n a {\n color: red;\n }\n}\n" +); +test!( + query_not_paren, + "@media not (color) { + a { + color: red; + } + }", + "@media not (color) {\n a {\n color: red;\n }\n}\n" +); +test!( + many_parens, + "@media (((color))) { + a { + color: red; + } + }", + "@media (((color))) {\n a {\n color: red;\n }\n}\n" +); +test!( + many_parens_around_and, + "@media ((screen and (color))) { + a { + color: red; + } + }", + "@media ((color)) {\n a {\n color: red;\n }\n}\n" +); +test!( + newline_between_media_rules_declared_at_root_inside_each, + "@each $a in 1 2 3 { + a { + @media foo { + b { + color: $a; + } + } + + color: foo; + } + }", + "a {\n color: foo;\n}\n@media foo {\n a b {\n color: 1;\n }\n}\n\na {\n color: foo;\n}\n@media foo {\n a b {\n color: 2;\n }\n}\n\na {\n color: foo;\n}\n@media foo {\n a b {\n color: 3;\n }\n}\n" +); +test!( + newline_between_media_rules_declared_at_root_inside_each_with_preceding_style_rule, + "@each $a in 1 2 { + a { + color: red; + } + + @media foo { + a { + color: $a; + } + } + }", + "a {\n color: red;\n}\n\n@media foo {\n a {\n color: 1;\n }\n}\na {\n color: red;\n}\n\n@media foo {\n a {\n color: 2;\n }\n}\n" +); +test!( + no_newline_between_media_rules_when_invisble_rule_between, + "a {} + + @media (min-width: 5px) { + a { + color: 1; + } + } + + a {} + + @media (min-width: 5px) { + a { + color: 1; + } + }", + "@media (min-width: 5px) {\n a {\n color: 1;\n }\n}\n@media (min-width: 5px) {\n a {\n color: 1;\n }\n}\n" +); +test!( + two_media_rules_in_content_block, + "@mixin foo() { + @content; + } + + @include foo { + @media foo { + a { + color: red; + } + } + @media foo { + b { + color: red; + } + } + }", + "@media foo {\n a {\n color: red;\n }\n}\n@media foo {\n b {\n color: red;\n }\n}\n" +); +test!( + splits_child_nodes_when_preceding_media, + "@media (foo) { + @media (prefers-reduced-motion: reduce) { + a { + transition: none; + } + } + + a { + color: red; + } + + a { + color: red; + } + }", + "@media (foo) and (prefers-reduced-motion: reduce) {\n a {\n transition: none;\n }\n}\n@media (foo) {\n a {\n color: red;\n }\n}\n@media (foo) {\n a {\n color: red;\n }\n}\n" +); +test!( + doesnt_split_child_nodes_when_trailing_media, + "@media (foo) { + a { + color: red; + } + + a { + color: red; + } + + @media (prefers-reduced-motion: reduce) { + a { + transition: none; + } + } + }", + "@media (foo) {\n a {\n color: red;\n }\n a {\n color: red;\n }\n}\n@media (foo) and (prefers-reduced-motion: reduce) {\n a {\n transition: none;\n }\n}\n" +); +test!( + #[ignore = "our is_invisible_check inside css tree is flawed here"] + doesnt_split_child_nodes_when_leading_but_invisible_media, + "@media (foo) { + @media (prefers-reduced-motion: reduce) {} + + a { + color: red; + } + + a { + color: red; + } + }", + "@media (foo) {\n a {\n color: red;\n }\n a {\n color: red;\n }\n}\n" +); +test!( + media_has_url_in_parens, + "@media (url) { + a { + color: red; + } + }", + "@media (url) {\n a {\n color: red;\n }\n}\n" +); +test!( + #[ignore = "our is_invisible_check inside css tree is flawed here"] + media_does_not_split_when_child_rule_has_invisible_media, + "@media (min-width: 1px) { + .first { + font-weight: 100; + + @media (min-width: 2px) {} + } + + .second { + font-weight: 200; + } + }", + "@media (url) {\n a {\n color: red;\n }\n}\n" +); +error!( + media_query_has_quoted_closing_paren, + r#"@media ('a)'w) { + a { + color: red; + } + }"#, + "Error: expected no more input." +); diff --git a/tests/meta-module.rs b/tests/meta-module.rs index bcba8d02..b4ea19cc 100644 --- a/tests/meta-module.rs +++ b/tests/meta-module.rs @@ -4,14 +4,15 @@ use std::io::Write; mod macros; test!( + #[ignore = "weird ordering problem"] module_functions_builtin, "@use 'sass:meta';\na {\n color: inspect(meta.module-functions(meta));\n}\n", - "a {\n color: (\"feature-exists\": get-function(\"feature-exists\"), \"inspect\": get-function(\"inspect\"), \"type-of\": get-function(\"type-of\"), \"keywords\": get-function(\"keywords\"), \"global-variable-exists\": get-function(\"global-variable-exists\"), \"variable-exists\": get-function(\"variable-exists\"), \"function-exists\": get-function(\"function-exists\"), \"mixin-exists\": get-function(\"mixin-exists\"), \"content-exists\": get-function(\"content-exists\"), \"module-variables\": get-function(\"module-variables\"), \"module-functions\": get-function(\"module-functions\"), \"get-function\": get-function(\"get-function\"), \"call\": get-function(\"call\"));\n}\n" + "a {\n color: (\"feature-exists\": get-function(\"feature-exists\"), \"inspect\": get-function(\"inspect\"), \"type-of\": get-function(\"type-of\"), \"keywords\": get-function(\"keywords\"), \"global-variable-exists\": get-function(\"global-variable-exists\"), \"variable-exists\": get-function(\"variable-exists\"), \"function-exists\": get-function(\"function-exists\"), \"mixin-exists\": get-function(\"mixin-exists\"), \"content-exists\": get-function(\"content-exists\"), \"module-variables\": get-function(\"module-variables\"), \"module-functions\": get-function(\"module-functions\"), \"get-function\": get-function(\"get-function\"), \"call\": get-function(\"call\"), \"calc-args\": get-function(\"calc-args\"), \"calc-name\": get-function(\"calc-name\"));\n}\n" ); test!( module_variables_builtin, - "@use 'sass:meta';\n@use 'sass:math';\na {\n color: inspect(meta.module-variables(math));\n}\n", - "a {\n color: (\"e\": 2.7182818285, \"pi\": 3.1415926536);\n}\n" + "@use 'sass:meta';\n@use 'sass:math';\na {\n color: inspect(map-get(meta.module-variables(math), 'e'));\n}\n", + "a {\n color: 2.7182818285;\n}\n" ); test!( global_var_exists_module, diff --git a/tests/meta.rs b/tests/meta.rs index 27569638..138da9b8 100644 --- a/tests/meta.rs +++ b/tests/meta.rs @@ -73,7 +73,6 @@ test!( ); // Unignore as more features are added test!( - #[ignore] feature_exists_custom_property, "a {\n color: feature-exists(custom-property)\n}\n", "a {\n color: true;\n}\n" @@ -198,6 +197,11 @@ test!( "a {\n color: type-of((0 / 0))\n}\n", "a {\n color: number;\n}\n" ); +test!( + type_of_calculation, + "a {\n color: type-of(calc(var(--bs-border-width) * 2))\n}\n", + "a {\n color: calculation;\n}\n" +); test!( type_of_arglist, "@mixin foo($a...) {color: type-of($a);}\na {@include foo(1, 2, 3, 4, 5);}", @@ -310,3 +314,5 @@ test!( }", "a {\n color: true;\n}\n" ); + +// todo: if() with different combinations of named and positional args diff --git a/tests/min-max.rs b/tests/min-max.rs index 6071ca63..c468d041 100644 --- a/tests/min-max.rs +++ b/tests/min-max.rs @@ -4,17 +4,17 @@ mod macros; test!( min_not_evaluated_units_percent, "a {\n color: min(1%, 2%);\n}\n", - "a {\n color: min(1%, 2%);\n}\n" + "a {\n color: 1%;\n}\n" ); test!( min_not_evaluated_units_px, "a {\n color: min(1px, 2px);\n}\n", - "a {\n color: min(1px, 2px);\n}\n" + "a {\n color: 1px;\n}\n" ); test!( min_not_evaluated_no_units, "a {\n color: min(1, 2);\n}\n", - "a {\n color: min(1, 2);\n}\n" + "a {\n color: 1;\n}\n" ); test!( min_not_evaluated_incompatible_units, @@ -44,35 +44,34 @@ error!( min_too_few_args, "a {\n color: min();\n}\n", "Error: At least one argument must be passed." ); -// note: we explicitly have units in the opposite order of `dart-sass`. -// see https://github.com/sass/dart-sass/issues/766 -error!( +test!( min_incompatible_units, - "$a: 1px;\n$b: 2%;\na {\n color: min($a, $b);\n}\n", "Error: Incompatible units px and %." + "$a: 1px;\n$b: 2%;\na {\n color: min($a, $b);\n}\n", + "a {\n color: min(1px, 2%);\n}\n" ); test!( - max_not_evaluated_units_percent, + max_same_units_percent, "a {\n color: max(1%, 2%);\n}\n", - "a {\n color: max(1%, 2%);\n}\n" + "a {\n color: 2%;\n}\n" ); test!( - max_not_evaluated_units_px, + max_same_units_px, "a {\n color: max(1px, 2px);\n}\n", - "a {\n color: max(1px, 2px);\n}\n" + "a {\n color: 2px;\n}\n" ); test!( - max_not_evaluated_no_units, + max_same_units_none, "a {\n color: max(1, 2);\n}\n", - "a {\n color: max(1, 2);\n}\n" + "a {\n color: 2;\n}\n" ); test!( - max_not_evaluated_incompatible_units, + max_uncomparable_but_compatible_units, "a {\n color: max(1%, 2vh);\n}\n", "a {\n color: max(1%, 2vh);\n}\n" ); test!( max_not_evaluated_interpolation, - "$a: 1%;\n$b: 2%;\na {\n color: max(#{$a}, #{$b});;\n}\n", + "$a: 1%;\n$b: 2%;\na {\n color: max(#{$a}, #{$b});\n}\n", "a {\n color: max(1%, 2%);\n}\n" ); test!( @@ -98,37 +97,36 @@ error!( max_too_few_args, "a {\n color: max();\n}\n", "Error: At least one argument must be passed." ); -// note: we explicitly have units in the opposite order of `dart-sass`. -// see https://github.com/sass/dart-sass/issues/766 -error!( +test!( max_incompatible_units, - "$a: 1px;\n$b: 2%;\na {\n color: max($a, $b);\n}\n", "Error: Incompatible units px and %." + "$a: 1px;\n$b: 2%;\na {\n color: max($a, $b);\n}\n", + "a {\n color: max(1px, 2%);\n}\n" ); // todo: special functions, min(calc(1), $b); test!( min_containing_max, "a {\n color: min(1, max(2));\n}\n", - "a {\n color: min(1, max(2));\n}\n" + "a {\n color: 1;\n}\n" ); test!( max_containing_min, "a {\n color: max(1, min(2));\n}\n", - "a {\n color: max(1, min(2));\n}\n" + "a {\n color: 2;\n}\n" ); test!( min_containing_max_as_only_arg, "a {\n color: min(max(1px, 2px));\n}\n", - "a {\n color: min(max(1px, 2px));\n}\n" + "a {\n color: 2px;\n}\n" ); test!( max_containing_min_as_only_arg, "a {\n color: max(min(1px, 2px));\n}\n", - "a {\n color: max(min(1px, 2px));\n}\n" + "a {\n color: 1px;\n}\n" ); test!( extremely_nested_min_and_max, "a {\n color: min(max(min(max(min(min(1), max(2))))), min(max(min(3))));\n}\n", - "a {\n color: min(max(min(max(min(min(1), max(2))))), min(max(min(3))));\n}\n" + "a {\n color: 1;\n}\n" ); test!( decimal_without_leading_integer_is_evaluated, @@ -138,69 +136,79 @@ test!( test!( decimal_with_leading_integer_is_not_evaluated, "a {\n color: min(0.2, 0.4);\n}\n", - "a {\n color: min(0.2, 0.4);\n}\n" + "a {\n color: 0.2;\n}\n" ); test!( - min_conains_special_fn_env, + min_contains_special_fn_env, "a {\n color: min(env(\"foo\"));\n}\n", "a {\n color: min(env(\"foo\"));\n}\n" ); test!( - min_conains_special_fn_calc_with_div_and_spaces, + min_contains_special_fn_calc_with_div_and_spaces, "a {\n color: min(calc(1 / 2));\n}\n", - "a {\n color: min(calc(1 / 2));\n}\n" + "a {\n color: 0.5;\n}\n" ); test!( - min_conains_special_fn_calc_with_div_without_spaces, + min_contains_special_fn_calc_with_div_without_spaces, "a {\n color: min(calc(1/2));\n}\n", - "a {\n color: min(calc(1/2));\n}\n" + "a {\n color: 0.5;\n}\n" ); -test!( - min_conains_special_fn_calc_with_plus_only, - "a {\n color: min(calc(+));\n}\n", - "a {\n color: min(calc(+));\n}\n" +error!( + min_contains_special_fn_calc_with_plus_only, + "a {\n color: min(calc(+));\n}\n", "Error: Expected digit." ); -test!( - min_conains_special_fn_calc_space_separated_list, - "a {\n color: min(calc(1 2));\n}\n", - "a {\n color: min(calc(1 2));\n}\n" +error!( + min_contains_special_fn_calc_space_separated_list, + "a {\n color: min(calc(1 2));\n}\n", r#"Error: expected "+", "-", "*", "/", or ")"."# ); test!( - min_conains_special_fn_var, + min_contains_special_fn_var, "a {\n color: min(1, var(--foo));\n}\n", "a {\n color: min(1, var(--foo));\n}\n" ); test!( - min_conains_multiline_comment, - "a {\n color: min(1/**/);\n}\n", - "a {\n color: min(1);\n}\n" + max_contains_special_fn_var, + "a {\n color: max(1, var(--foo));\n}\n", + "a {\n color: max(1, var(--foo));\n}\n" ); test!( - min_conains_calc_contains_multiline_comment, - "a {\n color: min(calc(1 /**/ 2));\n}\n", - "a {\n color: min(calc(1 /**/ 2));\n}\n" + min_contains_multiline_comment, + "a {\n color: min(1/**/);\n}\n", + "a {\n color: 1;\n}\n" +); +error!( + min_contains_calc_contains_multiline_comment, + "a {\n color: min(calc(1 /**/ 2));\n}\n", r#"Error: expected "+", "-", "*", "/", or ")"."# ); test!( - #[ignore = "we currently resolve interpolation eagerly inside loud comments"] - min_conains_calc_contains_multiline_comment_with_interpolation, - "a {\n color: min(calc(1 /* #{5} */ 2));\n}\n", - "a {\n color: min(calc(1 /* #{5} */ 2));\n}\n" + min_contains_calc_contains_multiline_comment_with_interpolation, + "a {\n color: min(calc(1 + /* #{5} */ 2));\n}\n", + "a {\n color: 3;\n}\n" ); test!( min_uppercase, "a {\n color: MIN(1);\n}\n", - "a {\n color: min(1);\n}\n" + "a {\n color: 1;\n}\n" ); test!( max_uppercase, "a {\n color: MAX(1);\n}\n", - "a {\n color: max(1);\n}\n" + "a {\n color: 1;\n}\n" ); - test!( min_parenthesis_around_arg, "a {\n color: min((1));\n}\n", - "a {\n color: min((1));\n}\n" + "a {\n color: 1;\n}\n" +); +test!( + max_compatible_units_does_conversion, + "a {\n color: max(1px, 1in, 1cm);\n}\n", + "a {\n color: 1in;\n}\n" +); +test!( + min_compatible_units_does_conversion, + "a {\n color: min(1px, 1in, 1cm);\n}\n", + "a {\n color: 1px;\n}\n" ); error!( min_parenthesis_around_arg_with_comma, @@ -242,8 +250,8 @@ error!( min_min_invalid, "a {\n color: min(min(#));\n}\n", "Error: Expected identifier." ); -test!( +error!( min_calc_parens_no_args, "a {\n color: min(calc());\n}\n", - "a {\n color: min(calc());\n}\n" + "Error: Expected number, variable, function, or calculation." ); diff --git a/tests/mixins.rs b/tests/mixins.rs index 5cb1fe93..817f0593 100644 --- a/tests/mixins.rs +++ b/tests/mixins.rs @@ -363,7 +363,7 @@ error!( "Error: Missing argument $a." ); test!( - inner_mixin_can_modify_scope, + inner_mixin_can_have_scope_modified, "a { $a: red; @mixin foo { @@ -495,10 +495,10 @@ test!( test!( content_contains_variable_declared_in_outer_scope_not_declared_at_root_and_modified, "a { - $a: red; + $a: wrong; @mixin foo { - $a: green; + $a: correct; @content; } @@ -506,24 +506,24 @@ test!( color: $a; } }", - "a {\n color: green;\n}\n" + "a {\n color: correct;\n}\n" ); test!( content_contains_variable_declared_in_outer_scope_declared_at_root_and_modified, "@mixin foo { - $a: green; + $a: wrong; @content; } a { - $a: red; + $a: correct; @include foo { color: $a; } }", - "a {\n color: red;\n}\n" + "a {\n color: correct;\n}\n" ); test!( content_default_arg_value_no_parens, @@ -563,6 +563,90 @@ test!( }", "a {\n color: red;\n}\n" ); +test!( + mixin_cant_affect_scope_in_which_it_was_included, + "@mixin test { + $a: wrong; + } + + a { + $a: correct; + @include test; + color: $a; + }", + "a {\n color: correct;\n}\n" +); +test!( + content_block_has_two_rulesets, + "@mixin foo() { + @content; + } + + @include foo { + a { + color: red; + } + + b { + color: red; + } + }", + "a {\n color: red;\n}\n\nb {\n color: red;\n}\n" +); +test!( + mixin_has_two_rulesets, + "@mixin foo() { + a { + display: none; + } + + b { + display: block; + } + } + + @include foo();", + "a {\n display: none;\n}\n\nb {\n display: block;\n}\n" +); +test!( + sass_spec__188_test_mixin_content, + "$color: blue; + + @mixin context($class, $color: red) { + .#{$class} { + background-color: $color; + @content; + border-color: $color; + } + } + + @include context(parent) { + @include context(child, $color: yellow) { + color: $color; + } + }", + ".parent {\n background-color: red;\n border-color: red;\n}\n.parent .child {\n background-color: yellow;\n color: blue;\n border-color: yellow;\n}\n" +); +test!( + sass_spec__mixin_environment_locality, + r#"// The "$var" variable should only be set locally, despite being in the same + // mixin each time. + @mixin with-local-variable($recurse) { + $var: before; + + @if ($recurse) { + @include with-local-variable($recurse: false); + } + + var: $var; + $var: after; + } + + .environment-locality { + @include with-local-variable($recurse: true); + }"#, + ".environment-locality {\n var: before;\n var: before;\n}\n" +); error!( mixin_in_function, "@function foo() { @@ -603,3 +687,10 @@ error!( }", "Error: expected \"{\"." ); +error!( + disallows_content_block_when_mixin_has_no_content_block, + "@mixin foo () {} + @include foo {} + ", + "Error: Mixin doesn't accept a content block." +); diff --git a/tests/modulo.rs b/tests/modulo.rs index 71a30e66..2ceed82c 100644 --- a/tests/modulo.rs +++ b/tests/modulo.rs @@ -83,10 +83,100 @@ test!( test!( big_negative_mod_positive, "a {\n color: -99999990000099999999999999 % 2;\n}\n", - "a {\n color: 1;\n}\n" + "a {\n color: 0;\n}\n" ); test!( big_int_result_is_equal_to_small_int, "a {\n color: (6 % 2) == 0;\n}\n", "a {\n color: true;\n}\n" ); +test!( + comparable_units_denom_0, + "a {\n color: 1in % 0px;\n}\n", + "a {\n color: NaNin;\n}\n" +); +test!( + comparable_units_negative_denom_0, + "a {\n color: -1in % 0px;\n}\n", + "a {\n color: NaNin;\n}\n" +); +test!( + comparable_units_both_positive, + "a {\n color: 1in % 1px;\n}\n", + "a {\n color: 0in;\n}\n" +); +test!( + comparable_units_denom_negative, + "a {\n color: 1in % -1px;\n}\n", + "a {\n color: -0.0104166667in;\n}\n" +); +test!( + comparable_units_both_negative, + "a {\n color: -1in % -1px;\n}\n", + "a {\n color: 0in;\n}\n" +); +test!( + comparable_units_numer_negative, + "a {\n color: -1in % 1px;\n}\n", + "a {\n color: 0.0104166667in;\n}\n" +); +test!( + comparable_units_both_0, + "a {\n color: 0in % 0px;\n}\n", + "a {\n color: NaNin;\n}\n" +); +test!( + nan_mod_positive_finite, + "a {\n color: (0/0) % 5;\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + nan_mod_negative_finite, + "a {\n color: (0/0) % -5;\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + infinity_mod_positive_finite, + "a {\n color: (1/0) % 5;\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + infinity_mod_negative_finite, + "a {\n color: (1/0) % -5;\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + positive_finite_mod_nan, + "a {\n color: 5 % (0/0);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + negative_finite_mod_nan, + "a {\n color: -5 % (0/0);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + positive_finite_mod_infinity, + "a {\n color: 5 % (1/0);\n}\n", + "a {\n color: 5;\n}\n" +); +test!( + negative_finite_mod_infinity, + "a {\n color: -5 % (1/0);\n}\n", + "a {\n color: Infinity;\n}\n" +); +test!( + positive_finite_mod_negative_infinity, + "a {\n color: 5 % (-1/0);\n}\n", + "a {\n color: -Infinity;\n}\n" +); +test!( + negative_finite_mod_negative_infinity, + "a {\n color: -5 % (-1/0);\n}\n", + "a {\n color: NaN;\n}\n" +); +error!( + calculation_mod_calculation, + "a {\n color: calc(1rem + 1px) % calc(1rem + 1px);\n}\n", + r#"Error: Undefined operation "calc(1rem + 1px) % calc(1rem + 1px)"."# +); diff --git a/tests/multiplication.rs b/tests/multiplication.rs index b3130478..41042b5e 100644 --- a/tests/multiplication.rs +++ b/tests/multiplication.rs @@ -23,3 +23,27 @@ error!( null_mul_number, "a {color: null * 1;}", "Error: Undefined operation \"null * 1\"." ); +error!( + calculation_mul_calculation, + "a {color: calc(1rem + 1px) * calc(1rem + 1px);}", + r#"Error: Undefined operation "calc(1rem + 1px) * calc(1rem + 1px)"."# +); +error!( + num_mul_calculation, + "a {color: 1 * calc(1rem + 1px);}", r#"Error: Undefined operation "1 * calc(1rem + 1px)"."# +); +test!( + num_mul_nan, + "a {\n color: 1 * (0/0);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + nan_mul_num, + "a {\n color: (0/0) * 1;\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + nan_mul_nan, + "a {\n color: (0/0) * (0/0);\n}\n", + "a {\n color: NaN;\n}\n" +); diff --git a/tests/nan.rs b/tests/nan.rs index 21a21d79..10b8b1a0 100644 --- a/tests/nan.rs +++ b/tests/nan.rs @@ -80,12 +80,12 @@ test!( error!( unitful_nan_str_slice_start, "@use \"sass:math\";\na {\n color: str-slice(\"\", math.acos(2));\n}\n", - "Error: $start: Expected NaNdeg to have no units." + "Error: $start-at: Expected NaNdeg to have no units." ); error!( unitful_nan_str_slice_end, "@use \"sass:math\";\na {\n color: str-slice(\"\", 0, math.acos(2));\n}\n", - "Error: $end: Expected NaNdeg to have no units." + "Error: $end-at: Expected NaNdeg to have no units." ); error!( unitful_nan_str_insert_index, @@ -115,39 +115,40 @@ test!( "a {\n color: NaNdeg;\n}\n" ); error!( + #[ignore = "we dont emit units"] unitful_nan_random, "@use \"sass:math\";\na {\n color: random(math.acos(2));\n}\n", "Error: $limit: NaNdeg is not an int." ); -test!( +error!( unitful_nan_min_first_arg, "@use \"sass:math\";\na {\n color: min(math.acos(2), 1px);\n}\n", - "a {\n color: NaNdeg;\n}\n" + "Error: NaNdeg and 1px are incompatible." ); -test!( +error!( unitful_nan_min_last_arg, "@use \"sass:math\";\na {\n color: min(1px, math.acos(2));\n}\n", - "a {\n color: 1px;\n}\n" + "Error: 1px and NaNdeg are incompatible." ); -test!( +error!( unitful_nan_min_middle_arg, "@use \"sass:math\";\na {\n color: min(1px, math.acos(2), 0);\n}\n", - "a {\n color: 0;\n}\n" + "Error: 1px and NaNdeg are incompatible." ); -test!( +error!( unitful_nan_max_first_arg, "@use \"sass:math\";\na {\n color: max(math.acos(2), 1px);\n}\n", - "a {\n color: NaNdeg;\n}\n" + "Error: NaNdeg and 1px are incompatible." ); -test!( +error!( unitful_nan_max_last_arg, "@use \"sass:math\";\na {\n color: max(1px, math.acos(2));\n}\n", - "a {\n color: 1px;\n}\n" + "Error: 1px and NaNdeg are incompatible." ); -test!( +error!( unitful_nan_max_middle_arg, "@use \"sass:math\";\na {\n color: max(1px, math.acos(2), 0);\n}\n", - "a {\n color: 1px;\n}\n" + "Error: 1px and NaNdeg are incompatible." ); error!( unitful_nan_nth_n, diff --git a/tests/not.rs b/tests/not.rs index c1c33997..09969ee1 100644 --- a/tests/not.rs +++ b/tests/not.rs @@ -36,3 +36,8 @@ test!( "a {\n color: not not false;\n}\n", "a {\n color: false;\n}\n" ); +test!( + not_calculation, + "a {\n color: not max(1px, 1vh);\n}\n", + "a {\n color: false;\n}\n" +); diff --git a/tests/null.rs b/tests/null.rs index 2b3cd27b..3177b6c3 100644 --- a/tests/null.rs +++ b/tests/null.rs @@ -24,7 +24,7 @@ test!( test!( bracketed_null_list_not_emitted, "a {\n color: [null null null];\n}\n", - "" + "a {\n color: [];\n}\n" ); test!( negative_null_in_var, diff --git a/tests/number.rs b/tests/number.rs index 4b93d485..fa8dd6c9 100644 --- a/tests/number.rs +++ b/tests/number.rs @@ -33,6 +33,21 @@ test!( "a {\n color: 1.0000;\n}\n", "a {\n color: 1;\n}\n" ); +test!( + unary_plus_on_integer, + "a {\n color: +1;\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + unary_plus_on_decimal, + "a {\n color: +1.5;\n}\n", + "a {\n color: 1.5;\n}\n" +); +test!( + unary_plus_on_scientific, + "a {\n color: +1e5;\n}\n", + "a {\n color: 100000;\n}\n" +); test!( many_nines_not_rounded, "a {\n color: 0.999999;\n}\n", @@ -167,17 +182,17 @@ test!( + 999999999999999999 + 999999999999999999 + 999999999999999999;\n}\n", - "a {\n color: 9999999999999999990;\n}\n" + "a {\n color: 10000000000000000000;\n}\n" ); test!( number_overflow_from_multiplication, "a {\n color: 999999999999999999 * 10;\n}\n", - "a {\n color: 9999999999999999990;\n}\n" + "a {\n color: 10000000000000000000;\n}\n" ); test!( number_overflow_from_division, "a {\n color: (999999999999999999 / .1);\n}\n", - "a {\n color: 9999999999999999990;\n}\n" + "a {\n color: 10000000000000000000;\n}\n" ); test!( bigint_is_equal_to_smallint, @@ -189,13 +204,17 @@ test!( }", "a {\n color: 0;\n color: true;\n}\n" ); -// we use arbitrary precision, so it is necessary to limit the size of exponents -// in order to prevent hangs -error!( - scientific_notation_too_positive, - "a {\n color: 1e100;\n}\n", "Error: Exponent too large." +test!( + #[ignore = "weird rounding issues"] + scientific_notation_very_large_positive, + "a {\n color: 1e100;\n}\n", "a {\n color: 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;\n}\n" +); +test!( + scientific_notation_very_large_negative, + "a {\n color: 1e-100;\n}\n", + "a {\n color: 0;\n}\n" ); error!( - scientific_notation_too_negative, - "a {\n color: 1e-100;\n}\n", "Error: Exponent too negative." + scientific_notation_no_number_after_decimal, + "a {\n color: 1.e3;\n}\n", "Error: Expected digit." ); diff --git a/tests/or.rs b/tests/or.rs index 671108b3..a3f2cefd 100644 --- a/tests/or.rs +++ b/tests/or.rs @@ -70,7 +70,12 @@ test!( "a {\n color: true or red % foo, red;\n}\n", "a {\n color: true, red;\n}\n" ); +test!( + chained_and_or, + "a {\n color: true and true or false and false;\n}\n", + "a {\n color: true;\n}\n" +); error!( properly_bubbles_error_when_invalid_char_after_or, - "a {\n color: true or? foo;\n}\n", "Error: expected \";\"." + "a {\n color: true or? foo;\n}\n", "Error: Expected expression." ); diff --git a/tests/ordering.rs b/tests/ordering.rs index 821f839b..d905d5a3 100644 --- a/tests/ordering.rs +++ b/tests/ordering.rs @@ -66,6 +66,26 @@ test!( "a {\n color: 2in > 1cm;\n}\n", "a {\n color: true;\n}\n" ); +test!( + takes_into_account_different_units, + "a {\n color: 2in < 1cm;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + infinity_gt_infinity, + "a {\n color: (1/0) > (1/0);\n}\n", + "a {\n color: false;\n}\n" +); +test!( + infinity_gt_neg_infinity, + "a {\n color: (1/0) > (-1/0);\n}\n", + "a {\n color: true;\n}\n" +); +test!( + nan_gt_nan, + "a {\n color: (0/0) > (0/0);\n}\n", + "a {\n color: false;\n}\n" +); error!( strings_not_comparable, "a {\n color: a > b;\n}\n", "Error: Undefined operation \"a > b\"." diff --git a/tests/plain-css-fn.rs b/tests/plain-css-fn.rs index 41935a30..d7387417 100644 --- a/tests/plain-css-fn.rs +++ b/tests/plain-css-fn.rs @@ -72,14 +72,14 @@ test!( "a {\n color: true;\n}\n" ); test!( - #[ignore = "this is not currently parsed correctly"] fn_named_and_alone_is_not_evaluated_as_binop, "a {\n color: and(foo);\n}\n", "a {\n color: and(foo);\n}\n" ); test!( - #[ignore = "this is not currently parsed correctly"] fn_named_or_alone_is_not_evaluated_as_binop, "a {\n color: or(foo);\n}\n", "a {\n color: or(foo);\n}\n" ); + +// todo: @function and($a) {} a { color: and(foo) } diff --git a/tests/plain-css.rs b/tests/plain-css.rs new file mode 100644 index 00000000..b456ce2d --- /dev/null +++ b/tests/plain-css.rs @@ -0,0 +1,47 @@ +use grass::InputSyntax; + +#[macro_use] +mod macros; + +test!( + function_call, + "a { + color: rotate(-45deg); + }", + "a {\n color: rotate(-45deg);\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + retains_null, + "a { + color: null; + }", + "a {\n color: null;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + does_not_evaluate_and, + "a { + color: 1 and 2; + }", + "a {\n color: 1 and 2;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + does_not_evaluate_or, + "a { + color: 1 or 2; + }", + "a {\n color: 1 or 2;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + does_not_evaluate_not, + "a { + color: not 2; + color: not true; + color: not false; + }", + "a {\n color: not 2;\n color: not true;\n color: not false;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); diff --git a/tests/sass.rs b/tests/sass.rs new file mode 100644 index 00000000..dde0b4b1 --- /dev/null +++ b/tests/sass.rs @@ -0,0 +1,92 @@ +use grass::InputSyntax; + +#[macro_use] +mod macros; + +test!( + two_properties, + r#" +a + color: red + foo: bar +"#, + "a {\n color: red;\n foo: bar;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + no_properties, + r#"a"#, + "", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + nested_styles, + r#" +a + color: red + b + foo: bar +"#, + "a {\n color: red;\n}\na b {\n foo: bar;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + nested_declarations, + r#" +a + color: red + foo: bar +"#, + "a {\n color: red;\n color-foo: bar;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + variable_declaration, + r#" +$a: red +a + color: $a +"#, + "a {\n color: red;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + silent_comment_before_variable_declaration, + r#" +// silent +$a: red + +a + color: $a +"#, + "a {\n color: red;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + two_silent_comments_before_variable_declaration, + r#" +// silent +// silent +$a: red + +a + color: $a +"#, + "a {\n color: red;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +test!( + unclosed_loud_comment, + r#"/* loud"#, + "/* loud */\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +error!( + multiline_comment_in_value_position, + r#" +$a: /* +loud */ red +"#, + "Error: expected */.", + grass::Options::default().input_syntax(InputSyntax::Sass) +); diff --git a/tests/selector-append.rs b/tests/selector-append.rs index 9a0de937..2a48e702 100644 --- a/tests/selector-append.rs +++ b/tests/selector-append.rs @@ -67,7 +67,7 @@ error!( error!( invalid_type_in_first_arg, "a {\n color: selector-append(\"c\", 1);\n}\n", - "Error: $selectors: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." + "Error: $selectors: 1 is not a valid selector: it must be a string," ); error!( no_args, diff --git a/tests/selector-nest.rs b/tests/selector-nest.rs index bb423140..470c61e0 100644 --- a/tests/selector-nest.rs +++ b/tests/selector-nest.rs @@ -157,12 +157,12 @@ error!( error!( unquoted_integer_first_arg, "a {\n color: selector-nest(1);\n}\n", - "Error: $selectors: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." + "Error: $selectors: 1 is not a valid selector: it must be a string," ); error!( unquoted_integer_second_arg, "a {\n color: selector-nest(\"c\", 1);\n}\n", - "Error: $selectors: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." + "Error: $selectors: 1 is not a valid selector: it must be a string," ); error!( empty_args, diff --git a/tests/selector-unify.rs b/tests/selector-unify.rs index a3185b9d..c7062c60 100644 --- a/tests/selector-unify.rs +++ b/tests/selector-unify.rs @@ -591,12 +591,12 @@ error!( error!( invalid_type_in_first_arg, "a {\n color: selector-unify(1, \"c\");\n}\n", - "Error: $selector1: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." + "Error: $selector1: 1 is not a valid selector: it must be a string," ); error!( invalid_type_in_second_arg, "a {\n color: selector-unify(\"c\", 1);\n}\n", - "Error: $selector2: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." + "Error: $selector2: 1 is not a valid selector: it must be a string," ); test!( simple_pseudo_no_arg_class_same, diff --git a/tests/selectors.rs b/tests/selectors.rs index ae69ff84..628126a3 100644 --- a/tests/selectors.rs +++ b/tests/selectors.rs @@ -397,9 +397,13 @@ test!( ); test!( combinator_alone, - "a {\n + {\n b {\n color: red;\n }\n}\n", + "a {\n + {\n b {\n color: red;\n }\n }\n}\n", "a + b {\n color: red;\n}\n" ); +error!( + combinator_alone_missing_closing_curly_brace, + "a {\n + {\n b {\n color: red;\n }\n}\n", "Error: expected \"}\"." +); test!( simple_multiple_newline, "a,\nb {\n color: red;\n}\n", @@ -441,15 +445,13 @@ test!( "#{&} a {\n color: red;\n}\n", "a {\n color: red;\n}\n" ); -test!( +error!( allows_id_start_with_number, - "#2foo {\n color: red;\n}\n", - "#2foo {\n color: red;\n}\n" + "#2foo {\n color: red;\n}\n", "Error: Expected identifier." ); -test!( +error!( allows_id_only_number, - "#2 {\n color: red;\n}\n", - "#2 {\n color: red;\n}\n" + "#2 {\n color: red;\n}\n", "Error: Expected identifier." ); test!( id_interpolation, @@ -782,6 +784,11 @@ test!( ":nth-child(ODD) {\n color: &;\n}\n", ":nth-child(odd) {\n color: :nth-child(odd);\n}\n" ); +test!( + a_n_plus_b_n_alone_of, + ":nth-child(n of a) {\n color: &;\n}\n", + ":nth-child(n of a) {\n color: :nth-child(n of a);\n}\n" +); test!( escaped_space_at_end_of_selector_immediately_after_pseudo_color, "a color:\\ {\n color: &;\n}\n", @@ -807,13 +814,16 @@ test!( "#{inspect(&)} {\n color: &;\n}\n", "null {\n color: null;\n}\n" ); +error!( + id_selector_starts_with_number, + "#2b {\n color: &;\n}\n", "Error: Expected identifier." +); test!( nth_of_type_mutliple_spaces_inside_parens_are_collapsed, ":nth-of-type(2 n - --1) {\n color: red;\n}\n", ":nth-of-type(2 n - --1) {\n color: red;\n}\n" ); test!( - #[ignore = "we do not yet have a good way of consuming a string without converting \\a to a newline"] silent_comment_in_quoted_attribute_value, ".foo bar[val=\"//\"] {\n color: &;\n}\n", ".foo bar[val=\"//\"] {\n color: .foo bar[val=\"//\"];\n}\n" @@ -831,11 +841,12 @@ test!( "[data-key=\"\\\\\"] {\n color: [data-key=\"\\\\\"];\n}\n" ); test!( - #[ignore = "we have to rewrite quoted attribute value parsing somewhat"] + #[ignore = "we have to rewrite quoted attribute serialization"] attribute_value_escape_ends_with_whitespace, - "[a=\"a\\\\66 \"] {\n color: &;\n}\n", + r#"[a="a\\66 "] { color: &;}"#, "[a=\"a\\\\66 \"] {\n color: [a=\"a\\\\66 \"];\n}\n" ); + test!( no_newline_between_styles_when_last_style_was_placeholder, "a { @@ -851,6 +862,29 @@ test!( }", "a {\n color: red;\n}\nc {\n color: red;\n}\n" ); +test!( + ambiguous_colon, + ".btn { + a:b { + color: red; + } + }", + ".btn a:b {\n color: red;\n}\n" +); +test!( + double_ampersand, + "a { + color: &&; + }", + "a {\n color: a a;\n}\n" +); +test!( + escaped_backslash_no_space_before_curly_brace, + r#"\\{ + color: &; + }"#, + "\\\\ {\n color: \\\\;\n}\n" +); error!( a_n_plus_b_n_invalid_odd, ":nth-child(ofdd) {\n color: &;\n}\n", "Error: Expected \"odd\"." @@ -867,6 +901,10 @@ error!( a_n_plus_b_n_invalid_char_after_even, ":nth-child(even#) {\n color: &;\n}\n", "Error: expected \")\"." ); +error!( + a_n_plus_b_n_nothing_after_plus, + ":nth-child:nth-child(n+{}", "Error: Expected a number." +); error!(nothing_after_period, ". {}", "Error: Expected identifier."); error!(nothing_after_hash, "# {}", "Error: Expected identifier."); error!(nothing_after_percent, "% {}", "Error: Expected identifier."); @@ -889,3 +927,17 @@ error!( denies_optional_in_selector, "a !optional {}", "Error: expected \"{\"." ); + +// todo: +// [attr=url] { +// color: red; +// } + +// [attr=unit] { +// color: red; +// } + +// todo: error test +// :nth-child(n/**/of a) { +// color: &; +// } diff --git a/tests/special-functions.rs b/tests/special-functions.rs index b50b0773..4b41807a 100644 --- a/tests/special-functions.rs +++ b/tests/special-functions.rs @@ -4,82 +4,93 @@ mod macros; test!( calc_whitespace, "a {\n color: calc( 1 );\n}\n", - "a {\n color: calc( 1 );\n}\n" + "a {\n color: 1;\n}\n" ); -test!( +error!( calc_newline, - "a {\n color: calc(\n);\n}\n", - "a {\n color: calc( );\n}\n" + "a {\n color: calc(\n);\n}\n", "Error: Expected number, variable, function, or calculation." ); -test!( +error!( calc_multiple_args, - "a {\n color: calc(1, 2, a, b, c);\n}\n", - "a {\n color: calc(1, 2, a, b, c);\n}\n" + "a {\n color: calc(1, 2, a, b, c);\n}\n", r#"Error: expected "+", "-", "*", "/", or ")"."# ); test!( - calc_does_not_evaluate_arithmetic, + calc_does_evaluate_arithmetic, "a {\n color: calc(1 + 2);\n}\n", - "a {\n color: calc(1 + 2);\n}\n" + "a {\n color: 3;\n}\n" +); +test!( + calc_operation_rhs_is_interpolation, + "a {\n color: calc(100% + (#{4px}));\n}\n", + "a {\n color: calc(100% + (4px));\n}\n" +); +test!( + calc_mul_negative_number, + "a {\n color: calc(var(--bs-border-width) * -1);\n}\n", + "a {\n color: calc(var(--bs-border-width) * -1);\n}\n" ); test!( calc_evaluates_interpolated_arithmetic, "a {\n color: calc(#{1 + 2});\n}\n", "a {\n color: calc(3);\n}\n" ); -test!( +error!( calc_retains_silent_comment, - "a {\n color: calc(//);\n}\n", - "a {\n color: calc(//);\n}\n" + "a {\n color: calc(//);\n}\n", "Error: Expected number, variable, function, or calculation." ); -test!( +error!( calc_retains_multiline_comment, - "a {\n color: calc(/**/);\n}\n", - "a {\n color: calc(/**/);\n}\n" + "a {\n color: calc(/**/);\n}\n", "Error: Expected number, variable, function, or calculation." ); -test!( +error!( calc_nested_parens, "a {\n color: calc((((()))));\n}\n", - "a {\n color: calc((((()))));\n}\n" + "Error: Expected number, variable, function, or calculation." ); test!( calc_invalid_arithmetic, "a {\n color: calc(2px + 2px + 5%);\n}\n", - "a {\n color: calc(2px + 2px + 5%);\n}\n" + "a {\n color: calc(4px + 5%);\n}\n" +); +test!( + calc_add_same_unit_opposite_sides_of_non_comparable_unit, + "a {\n color: calc(2px + 5% + 2px);\n}\n", + "a {\n color: calc(2px + 5% + 2px);\n}\n" ); test!( calc_uppercase, "a {\n color: CALC(1 + 1);\n}\n", - "a {\n color: calc(1 + 1);\n}\n" + "a {\n color: 2;\n}\n" ); test!( calc_mixed_casing, "a {\n color: cAlC(1 + 1);\n}\n", - "a {\n color: calc(1 + 1);\n}\n" + "a {\n color: 2;\n}\n" ); test!( calc_browser_prefixed, "a {\n color: -webkit-calc(1 + 2);\n}\n", "a {\n color: -webkit-calc(1 + 2);\n}\n" ); -test!( +error!( calc_quoted_string, - r#"a { color: calc("\ "); }"#, - "a {\n color: calc(\" \");\n}\n" + r#"a { color: calc("\ "); }"#, "Error: Expected number, variable, function, or calculation." ); -test!( +error!( calc_quoted_string_single_quoted_paren, - "a {\n color: calc(\")\");\n}\n", - "a {\n color: calc(\")\");\n}\n" + r#"a {color: calc(")");}"#, "Error: Expected number, variable, function, or calculation." ); -test!( +error!( calc_quoted_string_single_quotes, - "a {\n color: calc('a');\n}\n", - "a {\n color: calc(\"a\");\n}\n" + "a {\n color: calc('a');\n}\n", "Error: Expected number, variable, function, or calculation." ); -test!( +error!( calc_hash_no_interpolation, - "a {\n color: calc(#);\n}\n", - "a {\n color: calc(#);\n}\n" + "a {\n color: calc(#);\n}\n", "Error: Expected number, variable, function, or calculation." +); +error!( + calc_boolean, + "$a: true; a {\n color: calc($a);\n}\n", "Error: Value true can't be used in a calculation." ); test!( element_whitespace, @@ -230,27 +241,22 @@ test!( "a {\n color: PrOgId:foo(fff);\n}\n", "a {\n color: progid:foo(fff);\n}\n" ); -error!( - progid_nothing_after, - "a { color: progid:", "Error: expected \"(\"." -); test!( - clamp_empty_args, - "a {\n color: clamp();\n}\n", - "a {\n color: clamp();\n}\n" + calc_plus_minus, + "a {\n color: calc(1% + 3px - 2px);\n}\n", + "a {\n color: calc(1% + 3px - 2px);\n}\n" ); test!( - clamp_parens_in_args, - "a {\n color: clamp((()));\n}\n", - "a {\n color: clamp((()));\n}\n" + calc_num_plus_interpolation, + "a {\n color: calc(1 + #{c});\n}\n", + "a {\n color: calc(1 + c);\n}\n" ); -test!( - clamp_single_arg, - "a {\n color: clamp(1);\n}\n", - "a {\n color: clamp(1);\n}\n" +error!( + progid_nothing_after, + "a { color: progid:", "Error: expected \"(\"." ); -test!( - clamp_many_args, - "a {\n color: clamp(1, 2, 3);\n}\n", - "a {\n color: clamp(1, 2, 3);\n}\n" +error!( + calc_no_whitespace_between_operator, + "a {\n color: calc(1+1);\n}\n", + r#"Error: "+" and "-" must be surrounded by whitespace in calculations."# ); diff --git a/tests/splat.rs b/tests/splat.rs index 07a2cdb6..7cdaf738 100644 --- a/tests/splat.rs +++ b/tests/splat.rs @@ -63,12 +63,12 @@ error!( "a { color: red(((a: b): red)...); }", - "Error: (a: b) is not a string in ((a: b): red)." + "Error: Variable keyword argument map must have string keys." ); error!( splat_map_with_non_string_key_number, "a { color: red((1: red)...); }", - "Error: 1 is not a string in (1: red)." + "Error: Variable keyword argument map must have string keys." ); diff --git a/tests/styles.rs b/tests/styles.rs index bd476c4c..79e1e7d3 100644 --- a/tests/styles.rs +++ b/tests/styles.rs @@ -33,9 +33,13 @@ test!( ); test!( removes_empty_outer_styles, - "a {\n b {\n color: red;\n }\n", + "a {\n b {\n color: red;\n }\n }\n", "a b {\n color: red;\n}\n" ); +error!( + removes_empty_outer_styles_missing_closing_curly_brace, + "a {\n b {\n color: red;\n }\n", "Error: expected \"}\"." +); test!(removes_empty_styles, "a {}\n", ""); test!( doesnt_eat_style_after_ruleset, @@ -200,20 +204,74 @@ test!( ); // todo: many other strange edge cases like `.style: val` (dealing with ambiguity is hard for very little gain) test!( - #[ignore = "strange edge case"] style_begins_with_asterisk_without_whitespace, "a {\n *zoom: 1;\n}\n", "a {\n *zoom: 1;\n}\n" ); test!( - #[ignore = "strange edge case"] style_begins_with_asterisk_with_whitespace, "a {\n * zoom: 1;\n}\n", "a {\n * zoom: 1;\n}\n" ); test!( - #[ignore = "strange edge case"] style_begins_with_asterisk_with_newline, "a {\n * \n zoom: 1;\n}\n", "a {\n * \n zoom: 1;\n}\n" ); +test!( + no_newline_after_child_ruleset_ends_with_silent_child, + "a { + position: relative; + + b {} + } + + c { + white-space: nowrap; + }", + "a {\n position: relative;\n}\nc {\n white-space: nowrap;\n}\n" +); +error!( + media_inside_nested_declaration, + "a { + color: { + @media foo {} + } + }", + "Error: This at-rule is not allowed here." +); +error!( + media_inside_nested_declaration_from_mixin, + "@mixin foo() { + @media foo {} + } + + a { + color: { + @include foo(); + } + }", + "Error: Media rules may not be used within nested declarations." +); +error!( + ruleset_inside_nested_declaration_from_mixin, + "@mixin foo() { + a {} + } + + a { + color: { + @include foo(); + } + }", + "Error: Style rules may not be used within nested declarations." +); +error!( + style_at_the_toplevel_from_mixin, + "@mixin foo() { + color: red; + } + + @include foo();", + "Error: Declarations may only be used within style rules." +); diff --git a/tests/subtraction.rs b/tests/subtraction.rs index ea507fd0..854cacab 100644 --- a/tests/subtraction.rs +++ b/tests/subtraction.rs @@ -323,3 +323,12 @@ error!( "a {color: 1 - get-function(lighten);}", "Error: get-function(\"lighten\") isn't a valid CSS value." ); +error!( + subtract_two_calculations, + "a {color: calc(1rem + 1px) - calc(1rem + 1px);}", + r#"Error: Undefined operation "calc(1rem + 1px) - calc(1rem + 1px)"."# +); +error!( + num_minus_calculation, + "a {color: 1 - calc(1rem + 1px);}", r#"Error: Undefined operation "1 - calc(1rem + 1px)"."# +); diff --git a/tests/supports.rs b/tests/supports.rs index b17ef557..e46718de 100644 --- a/tests/supports.rs +++ b/tests/supports.rs @@ -15,7 +15,7 @@ test!( "@supports (a: b) {\n a {\n color: red;\n }\n}\na {\n color: green;\n}\n" ); test!( - newline_between_styles_inside, + no_newline_between_styles_inside, "@supports (-ms-ime-align: auto) { a { color: red; @@ -25,7 +25,7 @@ test!( color: green; } }", - "@supports (-ms-ime-align: auto) {\n a {\n color: red;\n }\n\n b {\n color: green;\n }\n}\n" + "@supports (-ms-ime-align: auto) {\n a {\n color: red;\n }\n b {\n color: green;\n }\n}\n" ); test!( no_newline_after_media, @@ -48,7 +48,7 @@ test!( color: red; } }", - "@supports (position: sticky) {\n a {\n color: red;\n }\n\n @media (min-width: 576px) {\n a {\n color: red;\n }\n\n a {\n color: red;\n }\n }\n a {\n color: red;\n }\n}\n" + "@supports (position: sticky) {\n a {\n color: red;\n }\n @media (min-width: 576px) {\n a {\n color: red;\n }\n a {\n color: red;\n }\n }\n a {\n color: red;\n }\n}\n" ); test!( newline_after_supports_when_inside_style_rule, @@ -63,3 +63,77 @@ test!( }", "@supports (position: sticky) {\n a {\n color: red;\n }\n}\n\na {\n color: red;\n}\n" ); +test!( + supports_nested_inside_media, + "@media foo { + @supports (a: b) { + a { + color: red; + } + } + }", + "@media foo {\n @supports (a: b) {\n a {\n color: red;\n }\n }\n}\n" +); +test!( + supports_nested_inside_style_rule, + "a { + @supports (a: b) { + b { + color: red; + } + } + }", + "@supports (a: b) {\n a b {\n color: red;\n }\n}\n" +); +test!( + supports_nested_inside_media_nested_inside_style_rule, + "a { + @media foo { + @supports (a: b) { + b { + color: red; + } + } + } + }", + "@media foo {\n @supports (a: b) {\n a b {\n color: red;\n }\n }\n}\n" +); +test!( + media_nested_inside_supports, + "@supports (a: b) { + @media foo { + a { + color: red; + } + } + }", + "@supports (a: b) {\n @media foo {\n a {\n color: red;\n }\n }\n}\n" +); +test!( + supports_nested_inside_supports, + "@supports (a: b) { + @supports (c: d) { + a { + color: red; + } + } + }", + "@supports (a: b) {\n @supports (c: d) {\n a {\n color: red;\n }\n }\n}\n" +); +test!( + supports_different_operation_is_in_parens, + "@supports (a: b) and ((c: d) or (e: f)) { + a { + color: red; + } + }", + "@supports (a: b) and ((c: d) or (e: f)) {\n a {\n color: red;\n }\n}\n" +); +test!( + supports_removed_if_all_children_invisible, + "@supports (a: b) { + %a {} + }", + "" +); +test!(supports_empty_body, "@supports (a: b) {}", ""); diff --git a/tests/unary.rs b/tests/unary.rs index 8abdc19d..0794f26b 100644 --- a/tests/unary.rs +++ b/tests/unary.rs @@ -91,3 +91,16 @@ test!( "a {\n color: -null;\n}\n", "a {\n color: -null;\n}\n" ); +test!( + unary_div_calculation, + "a {\n color: /calc(1rem + 1px);\n}\n", + "a {\n color: /calc(1rem + 1px);\n}\n" +); +error!( + unary_plus_calculation, + "a {\n color: +calc(1rem + 1px);\n}\n", r#"Error: Undefined operation "+calc(1rem + 1px)"."# +); +error!( + unary_neg_calculation, + "a {\n color: -(calc(1rem + 1px));\n}\n", r#"Error: Undefined operation "-calc(1rem + 1px)"."# +); diff --git a/tests/unicode-range.rs b/tests/unicode-range.rs index cfca504b..0e00d4f3 100644 --- a/tests/unicode-range.rs +++ b/tests/unicode-range.rs @@ -31,12 +31,12 @@ error!( ); error!( longer_than_6_characters, - "a {\n color: U+1234567;\n}\n", "Error: Expected end of identifier." + "a {\n color: U+1234567;\n}\n", "Error: Expected at most 6 digits." ); -// I believe this to be a bug in the dart-sass implementation -// we test for it to ensure full parity -test!( +error!( length_of_6_with_question_mark, - "a {\n color: U+123456?;\n}\n", - "a {\n color: U+123456?;\n}\n" + "a {\n color: U+123456?;\n}\n", "Error: Expected at most 6 digits." ); + +// todo: escaped u at start \75 and \55 +// with and without space diff --git a/tests/units.rs b/tests/units.rs index 9df8ff7e..ac40228d 100644 --- a/tests/units.rs +++ b/tests/units.rs @@ -186,12 +186,47 @@ test!( "a {\n color: comparable(1vw, 1vh);\n}\n", "a {\n color: false;\n}\n" ); +test!( + removes_same_unit_from_complex_in_division, + "a {\n color: ((1px*1px) / 1px);\n}\n", + "a {\n color: 1px;\n}\n" +); +test!( + removes_comparable_unit_from_complex_in_division_and_does_conversion, + "a {\n color: ((1in*1in) / 1cm);\n}\n", + "a {\n color: 2.54in;\n}\n" +); +test!( + add_complex_div_units, + "a {\n color: inspect((1em / 1em) + (1px / 1em));\n}\n", + "a {\n color: 2px/em;\n}\n" +); +test!( + #[ignore = "we need to rewrite how we compare and convert units"] + complex_units_with_same_denom_and_comparable_numer_are_comparable, + "a {\n color: comparable((23in/2fu), (23cm/2fu));\n}\n", + "a {\n color: true;\n}\n" +); +test!( + complex_unit_many_denom_one_numer, + "a {\n color: unit((1rem/1px) / 1vh);\n}\n", + "a {\n color: \"rem/px*vh\";\n}\n" +); error!( display_single_div_with_none_numerator, "a {\n color: (1 / 1em);\n}\n", "Error: 1em^-1 isn't a valid CSS value." ); error!( - #[ignore = "non-comparable inverse units"] + // note: dart-sass has error "Error: 1X and 1dppx have incompatible units." + capital_x_is_not_alias_for_dppx, + "a {\n color: 1X + 1dppx;\n}\n", "Error: Incompatible units dppx and X." +); +error!( + // note: dart-sass has error "Error: 1x and 1dppx have incompatible units." + lowercase_x_is_not_alias_for_dppx, + "a {\n color: 1x + 1dppx;\n}\n", "Error: Incompatible units dppx and x." +); +error!( display_single_div_with_non_comparable_numerator, "a {\n color: (1px / 1em);\n}\n", "Error: 1px/em isn't a valid CSS value." ); diff --git a/tests/unknown-at-rule.rs b/tests/unknown-at-rule.rs index c5375003..3ed6d6be 100644 --- a/tests/unknown-at-rule.rs +++ b/tests/unknown-at-rule.rs @@ -78,11 +78,32 @@ test!( "@b {}\n\na {\n color: red;\n}\n" ); test!( - #[ignore = "not sure how dart-sass is parsing this to include the semicolon in the params"] + parent_selector_moves_inside_rule, + "a { + @foo { + b: c + } + }", + "@foo {\n a {\n b: c;\n }\n}\n" +); +test!( + parent_selector_moves_inside_rule_and_is_parent_to_inner_selector, + "a { + @foo { + f { + b: c + } + } + }", + "@foo {\n a f {\n b: c;\n }\n}\n" +); +test!( params_contain_silent_comment_and_semicolon, "a { @box-shadow: $btn-focus-box-shadow, // $btn-active-box-shadow; }", - "a {\n @box-shadow : $btn-focus-box-shadow, / $btn-active-box-shadow;\n}\n" + "a {\n @box-shadow : $btn-focus-box-shadow, // $btn-active-box-shadow;;\n}\n" ); test!(contains_multiline_comment, "@foo /**/;\n", "@foo;\n"); + +// todo: test scoping in rule diff --git a/tests/use.rs b/tests/use.rs index da7be358..1db3072d 100644 --- a/tests/use.rs +++ b/tests/use.rs @@ -1,5 +1,7 @@ use std::io::Write; +use macros::TestFs; + #[macro_use] mod macros; @@ -42,6 +44,14 @@ error!( module_not_quoted_string, "@use a", "Error: Expected string." ); +error!( + use_file_name_is_invalid_identifier, + r#"@use "a b";"#, r#"Error: The default namespace "a b" is not a valid Sass identifier."# +); +error!( + use_empty_string, + r#"@use "";"#, r#"Error: The default namespace "" is not a valid Sass identifier."# +); test!( use_as, "@use \"sass:math\" as foo; @@ -66,6 +76,14 @@ test!( }", "a {\n color: -0.4161468365;\n}\n" ); +test!( + use_single_quotes, + "@use 'sass:math'; + a { + color: math.cos(2); + }", + "a {\n color: -0.4161468365;\n}\n" +); #[test] fn use_user_defined_same_directory() { @@ -220,7 +238,7 @@ fn use_as_with() { #[test] fn use_whitespace_and_comments() { - let input = "@use /**/ \"use_whitespace_and_comments\" /**/ as /**/ foo /**/ with /**/ ( /**/ $a /**/ : /**/ red /**/ ) /**/ ;"; + let input = "@use /**/ \"use_whitespace_and_comments\" /**/ as /**/ foo /**/ with /**/ ( /**/ $a /**/ : /**/ red /**/ );"; tempfile!( "use_whitespace_and_comments.scss", "$a: green !default; a { color: $a }" @@ -231,6 +249,16 @@ fn use_whitespace_and_comments() { ); } +#[test] +fn use_loud_comment_after_close_paren_with() { + let input = r#"@use "b" as foo with ($a : red) /**/ ;"#; + tempfile!( + "use_loud_comment_after_close_paren_with.scss", + "$a: green !default; a { color: $a }" + ); + assert_err!(r#"Error: expected ";"."#, input); +} + #[test] fn use_with_builtin_module() { let input = "@use \"sass:math\" with ($e: 2.7);"; @@ -342,9 +370,9 @@ fn use_can_see_modules_imported_by_other_modules_when_aliased_as_star() { "@use \"sass:math\";" ); - assert_eq!( - "a {\n color: 2.7182818285;\n}\n", - &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + assert_err!( + r#"Error: There is no module with the namespace "math"."#, + input ); } @@ -442,3 +470,117 @@ fn use_variable_declaration_between_use() { &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) ); } + +#[test] +fn include_mixin_with_star_namespace() { + let mut fs = TestFs::new(); + + fs.add_file( + "a.scss", + r#"@mixin foo() { + a { + color: red; + } + }"#, + ); + + let input = r#" + @use "a" as *; + + @include foo(); + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn include_variable_with_star_namespace() { + let mut fs = TestFs::new(); + + fs.add_file("a.scss", r#"$a: red;"#); + + let input = r#" + @use "a" as *; + + a { + color: $a; + } + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn include_function_with_star_namespace() { + let mut fs = TestFs::new(); + + fs.add_file( + "a.scss", + r#"@function foo() { + @return red; + }"#, + ); + + let input = r#" + @use "a" as *; + + a { + color: foo(); + } + "#; + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +#[test] +fn use_with_through_forward_multiple() { + let mut fs = TestFs::new(); + + fs.add_file( + "_used.scss", + r#" + @forward "left" with ($a: from used !default); + @forward "right" with ($b: from used !default); + "#, + ); + fs.add_file( + "_left.scss", + r#" + $a: from left !default; + + in-left { + c: $a + } + "#, + ); + fs.add_file( + "_right.scss", + r#" + $b: from left !default; + + in-right { + d: $b + } + "#, + ); + + let input = r#" + @use "used" with ($a: from input, $b: from input); + "#; + + assert_eq!( + "in-left {\n c: from input;\n}\n\nin-right {\n d: from input;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default().fs(&fs)).expect(input) + ); +} + +// todo: refactor these tests to use testfs where possible diff --git a/tests/variables.rs b/tests/variables.rs index fbaf125b..cae2fae3 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -383,11 +383,11 @@ error!( // note: dart-sass expects !important error!( no_value_only_flag, - "$a: !default;", "Error: Expected expression." + "$a: !default;", "Error: Expected \"important\"." ); error!( variable_value_after_flag, - "$a: !default red;", "Error: Expected expression." + "$a: !default red;", "Error: Expected \"important\"." ); error!( uppercase_flag, @@ -428,3 +428,5 @@ test!( }", "a {\n color: red;\n}\n" ); + +// todo: test that all scopes can affect global vars diff --git a/tests/warn.rs b/tests/warn.rs new file mode 100644 index 00000000..973fb969 --- /dev/null +++ b/tests/warn.rs @@ -0,0 +1,4 @@ +#[macro_use] +mod macros; + +test!(simple_warn, "@warn 2", ""); diff --git a/tests/while.rs b/tests/while.rs index dc33ac35..3cbca427 100644 --- a/tests/while.rs +++ b/tests/while.rs @@ -37,7 +37,21 @@ test!( ); test!( nested_while_not_at_root_scope, - "$continue_inner: true;\n$continue_outer: true;\n\nresult {\n @while $continue_outer {\n @while $continue_inner {\n $continue_inner: false;\n }\n\n $continue_outer: false;\n }\n\n continue_outer: $continue_outer;\n continue_inner: $continue_inner;\n}\n", + "$continue_inner: true; + $continue_outer: true; + + result { + @while $continue_outer { + @while $continue_inner { + $continue_inner: false; + } + + $continue_outer: false; + } + + continue_outer: $continue_outer; + continue_inner: $continue_inner; + }", "result {\n continue_outer: true;\n continue_inner: true;\n}\n" ); test!( @@ -130,5 +144,5 @@ test!( ); error!( missing_closing_curly_brace, - "@while true {", "Error: expected \"}\"." + "@while false {", "Error: expected \"}\"." );